@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +26 -0
  3. package/dist/actions.d.ts +14 -6
  4. package/dist/actions.js +7 -3
  5. package/dist/core/server/baseServer.d.ts +4 -1
  6. package/dist/core/server/baseServer.js +2 -1
  7. package/dist/core/server/closeOnSignals.d.ts +28 -0
  8. package/dist/core/server/closeOnSignals.js +46 -0
  9. package/dist/core/server/pageServer.js +3 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +2 -1
  12. package/dist/modules/actions/index.d.ts +12 -3
  13. package/dist/modules/actions/index.js +9 -3
  14. package/dist/modules/actions/jobs/index.d.ts +32 -0
  15. package/dist/modules/actions/jobs/index.js +57 -0
  16. package/dist/modules/actions/jobs/memoryQueue.d.ts +30 -0
  17. package/dist/modules/actions/jobs/memoryQueue.js +178 -0
  18. package/dist/modules/actions/jobs/memoryStores.test.d.ts +1 -0
  19. package/dist/modules/actions/jobs/scheduler.d.ts +62 -0
  20. package/dist/modules/actions/jobs/scheduler.js +125 -0
  21. package/dist/modules/actions/jobs/scheduler.test.d.ts +1 -0
  22. package/dist/modules/actions/jobs/schedules.d.ts +23 -0
  23. package/dist/modules/actions/jobs/schedules.js +47 -0
  24. package/dist/modules/actions/jobs/storeClock.d.ts +16 -0
  25. package/dist/modules/actions/jobs/storeClock.js +32 -0
  26. package/dist/modules/actions/jobs/testing/jobQueueContract.d.ts +25 -0
  27. package/dist/modules/actions/jobs/worker.d.ts +62 -0
  28. package/dist/modules/actions/jobs/worker.js +262 -0
  29. package/dist/modules/actions/jobs/worker.test.d.ts +1 -0
  30. package/dist/modules/actions/moduleFor.js +1 -0
  31. package/dist/modules/actions/runtime/precheck.js +1 -1
  32. package/dist/modules/actions/runtime/renderResolver.js +2 -2
  33. package/dist/modules/actions/transport/callHandler.js +1 -1
  34. package/dist/modules/actions/transport/webhookHandler.js +1 -1
  35. package/dist/modules/actions/types.d.ts +76 -1
  36. package/dist/modules/mongo/index.d.ts +4 -0
  37. package/dist/modules/mongo/jobQueue.d.ts +38 -0
  38. package/dist/modules/mongo/jobQueue.js +335 -0
  39. package/dist/modules/mongo/kv.d.ts +15 -0
  40. package/dist/modules/mongo/kv.js +98 -0
  41. package/dist/modules/mongo/stores.integration.test.d.ts +1 -0
  42. package/dist/modules/mysql/jobs/index.d.ts +5 -0
  43. package/dist/modules/mysql/jobs/jobQueue.d.ts +40 -0
  44. package/dist/modules/mysql/jobs/jobQueue.js +375 -0
  45. package/dist/modules/mysql/jobs/kv.d.ts +18 -0
  46. package/dist/modules/mysql/jobs/kv.js +78 -0
  47. package/dist/modules/mysql/jobs/schema.d.ts +24 -0
  48. package/dist/modules/mysql/jobs/schema.js +96 -0
  49. package/dist/modules/mysql/jobs/stores.integration.test.d.ts +1 -0
  50. package/dist/modules/ssr/Component.d.ts +2 -5
  51. package/dist/modules/ssr/Component.js +1 -2
  52. package/dist/modules/ssr/prepareRender.js +0 -3
  53. package/dist/mongo.d.ts +25 -0
  54. package/dist/mongo.js +3 -0
  55. package/dist/mysql.d.ts +11 -0
  56. package/dist/mysql.js +4 -1
  57. package/package.json +16 -6
  58. package/skills/plitzi-authoring/SKILL.md +73 -267
  59. package/skills/plitzi-authoring/reference/data-and-visibility.md +114 -0
  60. package/skills/plitzi-authoring/reference/elements-and-styles.md +107 -0
  61. package/skills/plitzi-authoring/reference/flows.md +80 -0
  62. package/skills/plitzi-authoring/reference/layouts.md +140 -0
  63. package/skills/plitzi-authoring/reference/review-checklist.md +50 -0
  64. package/skills/plitzi-authoring/reference/structure.md +83 -0
  65. package/skills/plitzi-authoring/reference/templates-and-export.md +54 -0
  66. package/skills/plitzi-authoring/reference/templates.md +69 -0
  67. package/skills/plitzi-authoring/reference/testing.md +52 -0
  68. package/dist/modules/actions/runtime/schedule.d.ts +0 -35
  69. package/dist/modules/actions/runtime/schedule.js +0 -76
  70. /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,4 @@
1
+ export { createMongoJobQueue } from './jobQueue';
2
+ export { createMongoKv } from './kv';
3
+ export type { MongoDbSource, MongoJobQueue, MongoJobQueueOptions } from './jobQueue';
4
+ export type { MongoKvOptions } from './kv';
@@ -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;