@plitzi/sdk-server 0.37.1 → 0.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -0
- package/README.md +26 -0
- package/dist/actions.d.ts +14 -6
- package/dist/actions.js +7 -3
- package/dist/core/server/baseServer.d.ts +4 -1
- package/dist/core/server/baseServer.js +2 -1
- package/dist/core/server/closeOnSignals.d.ts +28 -0
- package/dist/core/server/closeOnSignals.js +46 -0
- package/dist/core/server/pageServer.js +3 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/index.d.ts +12 -3
- package/dist/modules/actions/index.js +9 -3
- package/dist/modules/actions/jobs/index.d.ts +32 -0
- package/dist/modules/actions/jobs/index.js +57 -0
- package/dist/modules/actions/jobs/memoryQueue.d.ts +30 -0
- package/dist/modules/actions/jobs/memoryQueue.js +178 -0
- package/dist/modules/actions/jobs/memoryStores.test.d.ts +1 -0
- package/dist/modules/actions/jobs/scheduler.d.ts +62 -0
- package/dist/modules/actions/jobs/scheduler.js +125 -0
- package/dist/modules/actions/jobs/scheduler.test.d.ts +1 -0
- package/dist/modules/actions/jobs/schedules.d.ts +23 -0
- package/dist/modules/actions/jobs/schedules.js +47 -0
- package/dist/modules/actions/jobs/storeClock.d.ts +16 -0
- package/dist/modules/actions/jobs/storeClock.js +32 -0
- package/dist/modules/actions/jobs/testing/jobQueueContract.d.ts +25 -0
- package/dist/modules/actions/jobs/worker.d.ts +62 -0
- package/dist/modules/actions/jobs/worker.js +262 -0
- package/dist/modules/actions/jobs/worker.test.d.ts +1 -0
- package/dist/modules/actions/moduleFor.js +1 -0
- package/dist/modules/actions/runtime/precheck.js +1 -1
- package/dist/modules/actions/runtime/renderResolver.js +2 -2
- package/dist/modules/actions/transport/callHandler.js +1 -1
- package/dist/modules/actions/transport/webhookHandler.js +1 -1
- package/dist/modules/actions/types.d.ts +76 -1
- package/dist/modules/mongo/index.d.ts +4 -0
- package/dist/modules/mongo/jobQueue.d.ts +38 -0
- package/dist/modules/mongo/jobQueue.js +335 -0
- package/dist/modules/mongo/kv.d.ts +15 -0
- package/dist/modules/mongo/kv.js +98 -0
- package/dist/modules/mongo/stores.integration.test.d.ts +1 -0
- package/dist/modules/mysql/jobs/index.d.ts +5 -0
- package/dist/modules/mysql/jobs/jobQueue.d.ts +40 -0
- package/dist/modules/mysql/jobs/jobQueue.js +375 -0
- package/dist/modules/mysql/jobs/kv.d.ts +18 -0
- package/dist/modules/mysql/jobs/kv.js +78 -0
- package/dist/modules/mysql/jobs/schema.d.ts +24 -0
- package/dist/modules/mysql/jobs/schema.js +96 -0
- package/dist/modules/mysql/jobs/stores.integration.test.d.ts +1 -0
- package/dist/modules/ssr/Component.d.ts +2 -5
- package/dist/modules/ssr/Component.js +1 -2
- package/dist/modules/ssr/prepareRender.js +0 -3
- package/dist/mongo.d.ts +25 -0
- package/dist/mongo.js +3 -0
- package/dist/mysql.d.ts +11 -0
- package/dist/mysql.js +4 -1
- package/package.json +16 -6
- package/skills/plitzi-authoring/SKILL.md +73 -267
- package/skills/plitzi-authoring/reference/data-and-visibility.md +114 -0
- package/skills/plitzi-authoring/reference/elements-and-styles.md +107 -0
- package/skills/plitzi-authoring/reference/flows.md +80 -0
- package/skills/plitzi-authoring/reference/layouts.md +140 -0
- package/skills/plitzi-authoring/reference/review-checklist.md +50 -0
- package/skills/plitzi-authoring/reference/structure.md +83 -0
- package/skills/plitzi-authoring/reference/templates-and-export.md +54 -0
- package/skills/plitzi-authoring/reference/templates.md +69 -0
- package/skills/plitzi-authoring/reference/testing.md +52 -0
- package/dist/modules/actions/runtime/schedule.d.ts +0 -35
- package/dist/modules/actions/runtime/schedule.js +0 -76
- /package/dist/{modules/actions/runtime/schedule.test.d.ts → core/server/closeOnSignals.test.d.ts} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { ActionRunError } from "../runtime/errors.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
//#region src/modules/actions/jobs/worker.ts
|
|
4
|
+
/**
|
|
5
|
+
* Refusals that mean "not now" rather than "not ever".
|
|
6
|
+
*
|
|
7
|
+
* Both are the cluster protecting itself — another run holds the single-flight key, or this replica is already
|
|
8
|
+
* carrying as much as it will — and neither says anything about the job. So the job goes back to the queue with
|
|
9
|
+
* its attempt REFUNDED: burning one would let a busy afternoon retire a nightly digest as dead.
|
|
10
|
+
*/
|
|
11
|
+
var TRY_AGAIN = /* @__PURE__ */ new Set(["duplicate", "over_capacity"]);
|
|
12
|
+
var messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
13
|
+
/**
|
|
14
|
+
* Why a run that ended badly did, in the words an operator can act on: the step that failed and what it said.
|
|
15
|
+
*
|
|
16
|
+
* Read from `steps` — the outline, whose errors are already redacted of every credential the run resolved — so
|
|
17
|
+
* nothing lands on the job that the run history would not show. A run that failed without any step failing (a
|
|
18
|
+
* timeout between steps, a budget spent) has no step to name, and says how it ended instead.
|
|
19
|
+
*/
|
|
20
|
+
var failureOf = (result) => {
|
|
21
|
+
const failed = result.steps.find((step) => step.phase === "flow" && step.status === "failed");
|
|
22
|
+
return failed ? `step "${failed.id}" failed: ${failed.error ?? "no reason given"}` : `the flow ended ${result.status}`;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Refusals no number of attempts will change: the action was deleted, its schedule was switched off, or the
|
|
26
|
+
* document refuses this trigger. Retrying is noise, and the job stops where an operator can see why.
|
|
27
|
+
*/
|
|
28
|
+
var GIVE_UP = /* @__PURE__ */ new Set([
|
|
29
|
+
"not_found",
|
|
30
|
+
"disabled",
|
|
31
|
+
"forbidden",
|
|
32
|
+
"recursion"
|
|
33
|
+
]);
|
|
34
|
+
var createJobWorker = ({ queue, lookups, module, workers = 4, pollMs = 1e3, leaseMs = 3e4, backoff = {}, workerId = `${process.pid}-${randomUUID().slice(0, 8)}`, onError = (error) => console.error("[Actions] job worker failed:", error) }) => {
|
|
35
|
+
const { baseMs = 3e4, maxMs = 9e5 } = backoff;
|
|
36
|
+
const active = /* @__PURE__ */ new Map();
|
|
37
|
+
/** Every job this worker started, until it has settled — what `stop()` waits for. */
|
|
38
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
39
|
+
let timer;
|
|
40
|
+
let heart;
|
|
41
|
+
let stopping = false;
|
|
42
|
+
let pass;
|
|
43
|
+
const isStopping = () => stopping;
|
|
44
|
+
/** Doubles per attempt, with a jitter so a batch that failed together does not retry together. */
|
|
45
|
+
const retryDelay = (attempts) => {
|
|
46
|
+
const step = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempts - 1));
|
|
47
|
+
return step + Math.floor(Math.random() * Math.min(step, 3e4));
|
|
48
|
+
};
|
|
49
|
+
const settle = async (settlement) => {
|
|
50
|
+
try {
|
|
51
|
+
await queue.settle(settlement);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
onError(error);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** Back to the queue with nothing held against it — see {@link TRY_AGAIN}. */
|
|
57
|
+
const release = (job, delayMs, error) => settle({
|
|
58
|
+
jobId: job.id,
|
|
59
|
+
workerId,
|
|
60
|
+
status: "pending",
|
|
61
|
+
delayMs,
|
|
62
|
+
error
|
|
63
|
+
});
|
|
64
|
+
const fail = (job, runId, error) => {
|
|
65
|
+
const exhausted = job.attempts >= job.maxAttempts;
|
|
66
|
+
return settle({
|
|
67
|
+
jobId: job.id,
|
|
68
|
+
workerId,
|
|
69
|
+
status: exhausted ? "dead" : "pending",
|
|
70
|
+
...exhausted ? {} : { delayMs: retryDelay(job.attempts) },
|
|
71
|
+
...runId ? { runId } : {},
|
|
72
|
+
error,
|
|
73
|
+
attempt: {
|
|
74
|
+
status: "failed",
|
|
75
|
+
workerId,
|
|
76
|
+
...runId ? { runId } : {},
|
|
77
|
+
error
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
const execute = async (job) => {
|
|
82
|
+
const entry = await lookups.getAction(job.spaceId, job.actionId);
|
|
83
|
+
if (!entry) {
|
|
84
|
+
await settle({
|
|
85
|
+
jobId: job.id,
|
|
86
|
+
workerId,
|
|
87
|
+
status: "dead",
|
|
88
|
+
error: "the action this job belongs to no longer exists",
|
|
89
|
+
attempt: {
|
|
90
|
+
status: "failed",
|
|
91
|
+
workerId,
|
|
92
|
+
error: "action not found"
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let run;
|
|
98
|
+
try {
|
|
99
|
+
run = await module.guards.begin({
|
|
100
|
+
spaceId: job.spaceId,
|
|
101
|
+
actionId: job.actionId,
|
|
102
|
+
callerId: "schedule",
|
|
103
|
+
input: job.input,
|
|
104
|
+
idempotencyKey: job.id,
|
|
105
|
+
sharedKey: true,
|
|
106
|
+
ttlMs: module.limitsFor(entry.document).timeoutMs
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const refusal = error instanceof ActionRunError ? error.reason : void 0;
|
|
110
|
+
/**
|
|
111
|
+
* Two different things, and telling them apart is what decides whether the job survives.
|
|
112
|
+
*
|
|
113
|
+
* A REFUSAL is the cluster answering: another run holds the key, this replica is full. A throw that is not a
|
|
114
|
+
* refusal means the guards could not be consulted at all — the shared store is unreachable — and that is
|
|
115
|
+
* infrastructure rather than anything about this job. Both go back with the attempt REFUNDED.
|
|
116
|
+
*
|
|
117
|
+
* Counting an outage as attempts is how a queue loses work: with three attempts and a widening backoff, a
|
|
118
|
+
* Redis blip of two minutes retires every scheduled job in the cluster as `dead`, permanently, and the digest
|
|
119
|
+
* nobody got is indistinguishable from one nobody asked for. The job comes back instead, carrying why, and an
|
|
120
|
+
* operator sees a queue that is waiting rather than a queue that has been emptied.
|
|
121
|
+
*/
|
|
122
|
+
if (!refusal || TRY_AGAIN.has(refusal)) {
|
|
123
|
+
await release(job, retryDelay(1), `not started: ${refusal ?? messageOf(error)}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await fail(job, void 0, `could not start: ${refusal}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
active.set(job.id, run.controller);
|
|
130
|
+
try {
|
|
131
|
+
const result = await module.runAction({
|
|
132
|
+
entry,
|
|
133
|
+
input: job.input,
|
|
134
|
+
spaceId: job.spaceId,
|
|
135
|
+
environment: job.environment,
|
|
136
|
+
trigger: job.trigger,
|
|
137
|
+
callerId: "schedule",
|
|
138
|
+
runId: run.runId,
|
|
139
|
+
signal: run.controller.signal
|
|
140
|
+
});
|
|
141
|
+
if (result.status === "completed") {
|
|
142
|
+
await settle({
|
|
143
|
+
jobId: job.id,
|
|
144
|
+
workerId,
|
|
145
|
+
status: "succeeded",
|
|
146
|
+
runId: run.runId,
|
|
147
|
+
attempt: {
|
|
148
|
+
status: "succeeded",
|
|
149
|
+
workerId,
|
|
150
|
+
runId: run.runId
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (result.status === "aborted" && run.controller.signal.aborted) {
|
|
156
|
+
await settle({
|
|
157
|
+
jobId: job.id,
|
|
158
|
+
workerId,
|
|
159
|
+
status: "cancelled",
|
|
160
|
+
runId: run.runId,
|
|
161
|
+
attempt: {
|
|
162
|
+
status: "cancelled",
|
|
163
|
+
workerId,
|
|
164
|
+
runId: run.runId
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await fail(job, run.runId, failureOf(result));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const reason = error instanceof ActionRunError ? error.reason : void 0;
|
|
172
|
+
if (reason && GIVE_UP.has(reason)) {
|
|
173
|
+
await settle({
|
|
174
|
+
jobId: job.id,
|
|
175
|
+
workerId,
|
|
176
|
+
status: "dead",
|
|
177
|
+
runId: run.runId,
|
|
178
|
+
error: reason,
|
|
179
|
+
attempt: {
|
|
180
|
+
status: "failed",
|
|
181
|
+
workerId,
|
|
182
|
+
runId: run.runId,
|
|
183
|
+
error: reason
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await fail(job, run.runId, messageOf(error));
|
|
189
|
+
} finally {
|
|
190
|
+
active.delete(job.id);
|
|
191
|
+
await module.guards.end(run);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const poll = async () => {
|
|
195
|
+
const free = workers - active.size;
|
|
196
|
+
if (stopping || free <= 0) return 0;
|
|
197
|
+
const claimed = await queue.claim({
|
|
198
|
+
workerId,
|
|
199
|
+
leaseMs,
|
|
200
|
+
limit: free
|
|
201
|
+
});
|
|
202
|
+
if (isStopping()) {
|
|
203
|
+
await Promise.all(claimed.map((job) => release(job, 0, "not started: the replica was shutting down")));
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
for (const job of claimed) {
|
|
207
|
+
const running = execute(job).catch(onError);
|
|
208
|
+
inFlight.add(running);
|
|
209
|
+
running.finally(() => inFlight.delete(running));
|
|
210
|
+
}
|
|
211
|
+
return claimed.length;
|
|
212
|
+
};
|
|
213
|
+
/** Renews every claim this worker holds, and stops the ones an operator asked to cancel. */
|
|
214
|
+
const beat = async () => {
|
|
215
|
+
const held = [...active.keys()];
|
|
216
|
+
if (held.length === 0) return;
|
|
217
|
+
for (const jobId of await queue.heartbeat({
|
|
218
|
+
jobIds: held,
|
|
219
|
+
workerId,
|
|
220
|
+
leaseMs
|
|
221
|
+
})) active.get(jobId)?.abort();
|
|
222
|
+
};
|
|
223
|
+
/** One pass at a time, and the one under way is kept so `stop()` can wait for the claim it may be making. */
|
|
224
|
+
const guarded = async () => {
|
|
225
|
+
if (pass) return;
|
|
226
|
+
pass = poll().then(() => void 0).catch(onError).finally(() => {
|
|
227
|
+
pass = void 0;
|
|
228
|
+
});
|
|
229
|
+
await pass;
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
poll,
|
|
233
|
+
beat,
|
|
234
|
+
inFlight: () => active.size,
|
|
235
|
+
start: () => {
|
|
236
|
+
if (timer || workers <= 0) return;
|
|
237
|
+
timer = setInterval(() => void guarded(), pollMs);
|
|
238
|
+
timer.unref();
|
|
239
|
+
heart = setInterval(() => void beat().catch(onError), Math.max(1e3, Math.floor(leaseMs / 3)));
|
|
240
|
+
heart.unref();
|
|
241
|
+
},
|
|
242
|
+
stop: async () => {
|
|
243
|
+
stopping = true;
|
|
244
|
+
clearInterval(timer);
|
|
245
|
+
timer = void 0;
|
|
246
|
+
await pass;
|
|
247
|
+
if (inFlight.size > 0) console.info(`[Actions] waiting for ${inFlight.size} running job(s) to finish before stopping`);
|
|
248
|
+
/**
|
|
249
|
+
* Drained rather than abandoned: a deploy finishes the jobs it started instead of leaving them to be retried.
|
|
250
|
+
*
|
|
251
|
+
* The heartbeat is stopped only AFTER, and that is the half that makes waiting safe. A job longer than its
|
|
252
|
+
* lease used to lose its claim the moment the drain began — another replica took it over and ran it again
|
|
253
|
+
* while this one was still finishing it, and the drain gave up at the lease regardless.
|
|
254
|
+
*/
|
|
255
|
+
await Promise.allSettled([...inFlight]);
|
|
256
|
+
clearInterval(heart);
|
|
257
|
+
heart = void 0;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
//#endregion
|
|
262
|
+
export { createJobWorker };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -23,6 +23,7 @@ var actionsModuleFor = (config) => {
|
|
|
23
23
|
limits: config.action?.limits,
|
|
24
24
|
concurrency: config.action?.concurrency,
|
|
25
25
|
kv: config.action?.kv,
|
|
26
|
+
jobs: config.action?.jobs,
|
|
26
27
|
rateLimit: config.action?.rateLimit,
|
|
27
28
|
idempotency: config.action?.idempotency,
|
|
28
29
|
dbDrivers: config.action?.dbDrivers,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { findTriggerNode, triggerParams } from "./triggers.js";
|
|
1
2
|
import { ActionRunError } from "./errors.js";
|
|
2
3
|
import { applyFields } from "./scope.js";
|
|
3
|
-
import { findTriggerNode, triggerParams } from "./triggers.js";
|
|
4
4
|
import { triggerAccess, triggerInput } from "@plitzi/sdk-shared/actions";
|
|
5
5
|
//#region src/modules/actions/runtime/precheck.ts
|
|
6
6
|
var authorize = (access, kind, user) => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { ActionRunError } from "./errors.js";
|
|
2
1
|
import { findTriggerNode, triggerParams } from "./triggers.js";
|
|
2
|
+
import { ActionRunError } from "./errors.js";
|
|
3
3
|
import onAbort from "../../../helpers/onAbort.js";
|
|
4
4
|
import { createRenderShare } from "./renderShare.js";
|
|
5
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
6
5
|
import { triggerCacheMs } from "@plitzi/sdk-shared/actions";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
//#region src/modules/actions/runtime/renderResolver.ts
|
|
8
8
|
/**
|
|
9
9
|
* A render run that resolved nothing, carrying what it did.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ActionRunError } from "../runtime/errors.js";
|
|
2
1
|
import { triggerParams } from "../runtime/triggers.js";
|
|
2
|
+
import { ActionRunError } from "../runtime/errors.js";
|
|
3
3
|
import { precheckRun } from "../runtime/precheck.js";
|
|
4
4
|
import onAbort from "../../../helpers/onAbort.js";
|
|
5
5
|
import { openStream, wantsStream } from "./stream.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ActionRunError } from "../runtime/errors.js";
|
|
2
1
|
import { findTriggerNode, triggerParams } from "../runtime/triggers.js";
|
|
2
|
+
import { ActionRunError } from "../runtime/errors.js";
|
|
3
3
|
import { precheckRun } from "../runtime/precheck.js";
|
|
4
4
|
import onAbort from "../../../helpers/onAbort.js";
|
|
5
5
|
import { reportReject } from "../runtime/report.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionEmailSender } from './runtime/email';
|
|
2
|
-
import { ActionEmailConfig, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionRunStep, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
|
|
2
|
+
import { ActionEmailConfig, ActionJobQueue, ActionEntry, ActionLimits, ActionRejectRecord, ActionRunRecord, ActionRunStatus, ActionRunStep, ActionTriggerType, ConnectorManifest, Environment, InteractionCallbackParam, InteractionNode, InteractionNodeStatus, SpaceRevision, SSRUser } from '@plitzi/sdk-shared';
|
|
3
3
|
/** Resolved secret material, as the connector engine already models it. */
|
|
4
4
|
export type ActionCredential = Record<string, string>;
|
|
5
5
|
/**
|
|
@@ -71,6 +71,15 @@ export type ActionLookups = {
|
|
|
71
71
|
getAction: (spaceId: number, actionId: string, at?: SpaceRevision) => Promise<ActionEntry | undefined>;
|
|
72
72
|
/** Only the builder's catalog, the MCP and the scheduler need the list; a page call never asks for it. */
|
|
73
73
|
listActions?: (spaceId: number, at?: SpaceRevision) => Promise<ActionEntry[]>;
|
|
74
|
+
/**
|
|
75
|
+
* Which spaces have something scheduled, for the periodic reconcile.
|
|
76
|
+
*
|
|
77
|
+
* Optional, and a safety net rather than the mechanism: a deployment keeps the schedule rows current by calling
|
|
78
|
+
* `reconcile` whenever a space's actions change, and this is what catches the write that did not happen — a
|
|
79
|
+
* restore from backup, a row edited by hand, a deploy that missed a save. Absent, nothing reconciles on a timer
|
|
80
|
+
* and the rows are exactly as correct as the calls that wrote them.
|
|
81
|
+
*/
|
|
82
|
+
listScheduledSpaces?: () => Promise<number[]>;
|
|
74
83
|
getCredential?: (spaceId: number, identifier: string) => Promise<ActionCredential | undefined>;
|
|
75
84
|
getConnector?: (spaceId: number, connectorId: string, at?: SpaceRevision) => Promise<ConnectorManifest | undefined>;
|
|
76
85
|
};
|
|
@@ -159,6 +168,15 @@ export type ActionsConfig = {
|
|
|
159
168
|
};
|
|
160
169
|
/** Where the `kv` tasks keep things. Omitted → an in-process Map, which is per-replica by definition. */
|
|
161
170
|
kv?: ActionKvAdapter;
|
|
171
|
+
/**
|
|
172
|
+
* Scheduled runs, and the durable queue they go through. See {@link ActionJobsConfig}.
|
|
173
|
+
*
|
|
174
|
+
* On by default wherever `lookups.listActions` is answerable, because a deployment that can list its actions can
|
|
175
|
+
* schedule them and the old behaviour — a `schedule` trigger that silently did nothing unless the deployment
|
|
176
|
+
* built a timer and a lock of its own — was a trigger the editor offered and the server did not honour. `false`
|
|
177
|
+
* turns it off for a replica that must only serve pages.
|
|
178
|
+
*/
|
|
179
|
+
jobs?: ActionJobsConfig | false;
|
|
162
180
|
/** Inbound webhooks are public, so they are counted per caller per minute. Default 60. */
|
|
163
181
|
rateLimit?: {
|
|
164
182
|
webhookPerMinute?: number;
|
|
@@ -200,6 +218,63 @@ export type ActionsConfig = {
|
|
|
200
218
|
};
|
|
201
219
|
fetchImpl?: typeof fetch;
|
|
202
220
|
};
|
|
221
|
+
/**
|
|
222
|
+
* Scheduled runs: how they are produced, how many run at once, and where they are kept while they wait.
|
|
223
|
+
*
|
|
224
|
+
* Supplying nothing still schedules — over an in-process queue, which is correct for one replica and silently
|
|
225
|
+
* wrong for two, since each gets its own copy of every schedule. A deployment with more than one supplies a
|
|
226
|
+
* {@link ActionJobQueue} over a store they share.
|
|
227
|
+
*/
|
|
228
|
+
export type ActionJobsConfig = {
|
|
229
|
+
/**
|
|
230
|
+
* Where jobs and schedules live. Omitted → this process's memory: nothing survives a restart, and a second
|
|
231
|
+
* replica produces the same fires all over again.
|
|
232
|
+
*/
|
|
233
|
+
queue?: ActionJobQueue;
|
|
234
|
+
/**
|
|
235
|
+
* How many jobs this replica runs at once. Default 4; `0` makes it a producer that never consumes.
|
|
236
|
+
*
|
|
237
|
+
* The number that decides whether a backlog drains in minutes or hours, and it belongs to the deployment because
|
|
238
|
+
* only it knows what its flows do — a queue of five hundred HTTP calls wants a far higher number than a queue of
|
|
239
|
+
* five hundred renders. It never widens the run guards: `concurrency.perSpace` still caps one space's runs per
|
|
240
|
+
* replica, and jobs over that ceiling go back to the queue rather than failing.
|
|
241
|
+
*/
|
|
242
|
+
workers?: number;
|
|
243
|
+
/** Whether this replica sweeps schedules into the queue at all. Default true. */
|
|
244
|
+
produce?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* The spaces this server schedules for, when it serves a known few.
|
|
247
|
+
*
|
|
248
|
+
* The self-hosted shape: one space, named once. A multi-tenant deployment answers the same question with
|
|
249
|
+
* {@link ActionLookups.listScheduledSpaces} instead, which wins when both are given. With neither, nothing tells
|
|
250
|
+
* the scheduler where to look and it says so at boot rather than sitting silent — a schedule that never fires is
|
|
251
|
+
* indistinguishable from a cron expression that is wrong.
|
|
252
|
+
*/
|
|
253
|
+
spaces?: number[];
|
|
254
|
+
/** How often a free worker looks for a job. Default 1s. */
|
|
255
|
+
pollMs?: number;
|
|
256
|
+
/** How often this replica looks for due schedules. Default 15s. */
|
|
257
|
+
schedulePollMs?: number;
|
|
258
|
+
/**
|
|
259
|
+
* How long a claim is held before another replica may take the job over — the failover window. Default 30s,
|
|
260
|
+
* renewed every third of it while a job runs.
|
|
261
|
+
*/
|
|
262
|
+
leaseMs?: number;
|
|
263
|
+
/** Attempts a scheduled job gets before it is left for an operator as `dead`. Default 3. */
|
|
264
|
+
maxAttempts?: number;
|
|
265
|
+
/** First retry delay, doubling per attempt, up to `maxMs`. Defaults to 30s and 15 minutes. */
|
|
266
|
+
backoff?: {
|
|
267
|
+
baseMs?: number;
|
|
268
|
+
maxMs?: number;
|
|
269
|
+
};
|
|
270
|
+
/** Which environment scheduled runs execute in. Default `main`. */
|
|
271
|
+
environment?: Environment;
|
|
272
|
+
/** How often schedules are re-derived from documents, where `listScheduledSpaces` allows it. Default 15 min. */
|
|
273
|
+
reconcileMs?: number;
|
|
274
|
+
/** Names this replica in the job history. Defaults to the pid and a random suffix. */
|
|
275
|
+
workerId?: string;
|
|
276
|
+
onError?: (error: unknown) => void;
|
|
277
|
+
};
|
|
203
278
|
/** Re-exported so the module's own files import one place, and a deployment writing an `onRun` or an `onReject`
|
|
204
279
|
* sees the same shapes the module emits. */
|
|
205
280
|
export type { ActionEmailConfig, ActionEmailDelivery, ActionEmailMessage, ActionEmailTransport, ActionRejectRecord, ActionRunRecord } from '@plitzi/sdk-shared';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ActionJobQueue } from '@plitzi/sdk-shared';
|
|
2
|
+
import { Db } from 'mongodb';
|
|
3
|
+
/** The database, or how to reach it — a getter for a deployment whose client is replaced when it reconnects. */
|
|
4
|
+
export type MongoDbSource = Db | (() => Db);
|
|
5
|
+
export declare const resolveDb: (source: MongoDbSource) => Db;
|
|
6
|
+
export type MongoJobQueueOptions = {
|
|
7
|
+
db: MongoDbSource;
|
|
8
|
+
/** Collection names, for a deployment with a naming scheme of its own. */
|
|
9
|
+
collections?: {
|
|
10
|
+
jobs?: string;
|
|
11
|
+
schedules?: string;
|
|
12
|
+
};
|
|
13
|
+
/** How long a finished job stays readable. A waiting or running one never expires. Default 90 days. */
|
|
14
|
+
retentionDays?: number;
|
|
15
|
+
};
|
|
16
|
+
export type MongoJobQueue = ActionJobQueue & {
|
|
17
|
+
/** Takes the store's clock again on the next call — after a failover to another primary. */
|
|
18
|
+
resyncClock: () => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* An {@link ActionJobQueue} over two collections of a Mongo database the deployment owns.
|
|
22
|
+
*
|
|
23
|
+
* The helper for a deployment that already runs Mongo: it holds no connection of its own and creates nothing but the
|
|
24
|
+
* indexes its queries need, on first use. Every replica pointed at the same database shares the jobs and the
|
|
25
|
+
* schedules, which is what running more than one needs.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { createMongoJobQueue } from '@plitzi/sdk-server/mongo';
|
|
29
|
+
*
|
|
30
|
+
* const queue = createMongoJobQueue({ db: client.db('app') });
|
|
31
|
+
* createServer({ action: { lookups, kv, jobs: { queue } } });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The contract's rules, as Mongo keeps them: every instant is the SERVER's (`hello.localTime`), `enqueue` is idempotent
|
|
35
|
+
* by the job's `_id`, `claim` takes one document per `findOneAndUpdate` — the atomic unit — and reaps a lapsed lease in
|
|
36
|
+
* the same operation, and a schedule advances by compare-and-set on the instant that was read.
|
|
37
|
+
*/
|
|
38
|
+
export declare const createMongoJobQueue: ({ db, collections: names, retentionDays }: MongoJobQueueOptions) => MongoJobQueue;
|