@plitzi/sdk-server 0.37.0 → 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 +155 -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 +1 -4
  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,335 @@
1
+ import { createStoreClock } from "../actions/jobs/storeClock.js";
2
+ //#region src/modules/mongo/jobQueue.ts
3
+ /** Mongo's code for a unique index that refused a second document with the same key. */
4
+ var DUPLICATE_KEY = 11e3;
5
+ var TERMINAL = [
6
+ "succeeded",
7
+ "failed",
8
+ "dead",
9
+ "cancelled"
10
+ ];
11
+ var DAY_MS = 864e5;
12
+ var resolveDb = (source) => typeof source === "function" ? source() : source;
13
+ var scheduleId = (spaceId, actionId) => `${spaceId}:${actionId}`;
14
+ /**
15
+ * A stored document as the contract shapes it: an optional field is absent, never `null`.
16
+ *
17
+ * Mongo keeps what a `$set` wrote, and clearing a field in an update pipeline writes `null` — so a released job would
18
+ * otherwise come back with `workerId: null`, which every reader typed as `string | undefined` gets wrong.
19
+ */
20
+ var withoutNulls = (document) => Object.fromEntries(Object.entries(document).filter(([, value]) => value !== null));
21
+ var toJob = ({ _id, expiresAt: _expiresAt, ...rest }) => withoutNulls({
22
+ ...rest,
23
+ id: _id
24
+ });
25
+ var toSchedule = ({ _id: _unused, ...rest }) => withoutNulls(rest);
26
+ /** The history entry a reclaim writes on behalf of a worker that never came back. */
27
+ var lostAttempt = (at) => ({ $concatArrays: ["$history", [{
28
+ attempt: { $add: [{ $size: "$history" }, 1] },
29
+ status: "lost",
30
+ startedAt: "$updatedAt",
31
+ endedAt: at,
32
+ workerId: { $ifNull: ["$workerId", "unknown"] },
33
+ error: "the worker holding this job stopped reporting"
34
+ }]] });
35
+ /**
36
+ * An {@link ActionJobQueue} over two collections of a Mongo database the deployment owns.
37
+ *
38
+ * The helper for a deployment that already runs Mongo: it holds no connection of its own and creates nothing but the
39
+ * indexes its queries need, on first use. Every replica pointed at the same database shares the jobs and the
40
+ * schedules, which is what running more than one needs.
41
+ *
42
+ * ```ts
43
+ * import { createMongoJobQueue } from '@plitzi/sdk-server/mongo';
44
+ *
45
+ * const queue = createMongoJobQueue({ db: client.db('app') });
46
+ * createServer({ action: { lookups, kv, jobs: { queue } } });
47
+ * ```
48
+ *
49
+ * The contract's rules, as Mongo keeps them: every instant is the SERVER's (`hello.localTime`), `enqueue` is idempotent
50
+ * by the job's `_id`, `claim` takes one document per `findOneAndUpdate` — the atomic unit — and reaps a lapsed lease in
51
+ * the same operation, and a schedule advances by compare-and-set on the instant that was read.
52
+ */
53
+ var createMongoJobQueue = ({ db, collections: names = {}, retentionDays = 90 }) => {
54
+ const jobsName = names.jobs ?? "action_jobs";
55
+ const schedulesName = names.schedules ?? "action_schedules";
56
+ const jobsOf = () => resolveDb(db).collection(jobsName);
57
+ const schedulesOf = () => resolveDb(db).collection(schedulesName);
58
+ const clock = createStoreClock(async () => {
59
+ const { localTime } = await resolveDb(db).command({ hello: 1 });
60
+ return localTime ? localTime.getTime() : Date.now();
61
+ });
62
+ let ready;
63
+ /**
64
+ * The indexes the queue cannot work without, built once on first use.
65
+ *
66
+ * `claim` is the one that matters: it runs on every poll of every replica, and without an index over status and the
67
+ * two time fields it is a collection scan several times a second. The two branches of its `$or` are answered by one
68
+ * index each and merged already sorted.
69
+ */
70
+ const build = async () => {
71
+ await Promise.all([
72
+ jobsOf().createIndex({
73
+ status: 1,
74
+ runAt: 1
75
+ }),
76
+ jobsOf().createIndex({
77
+ status: 1,
78
+ leaseUntil: 1
79
+ }),
80
+ jobsOf().createIndex({
81
+ spaceId: 1,
82
+ updatedAt: -1
83
+ }),
84
+ jobsOf().createIndex({
85
+ spaceId: 1,
86
+ actionId: 1,
87
+ updatedAt: -1
88
+ }),
89
+ jobsOf().createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }),
90
+ schedulesOf().createIndex({
91
+ enabled: 1,
92
+ nextRunAt: 1
93
+ }),
94
+ schedulesOf().createIndex({ spaceId: 1 })
95
+ ]);
96
+ };
97
+ const collections = async () => {
98
+ ready ??= build().catch((error) => {
99
+ ready = void 0;
100
+ throw error;
101
+ });
102
+ await ready;
103
+ return {
104
+ jobs: jobsOf(),
105
+ schedules: schedulesOf()
106
+ };
107
+ };
108
+ const expiryOf = (at) => new Date(at + retentionDays * DAY_MS);
109
+ return {
110
+ resyncClock: clock.resync,
111
+ now: async () => new Date(await clock.now()),
112
+ enqueue: async (job) => {
113
+ const { jobs } = await collections();
114
+ const at = await clock.now();
115
+ const { id, ...fields } = job;
116
+ const document = {
117
+ ...fields,
118
+ _id: id,
119
+ status: "pending",
120
+ runAt: job.dueAt,
121
+ attempts: 0,
122
+ createdAt: at,
123
+ updatedAt: at,
124
+ history: []
125
+ };
126
+ try {
127
+ await jobs.insertOne(document);
128
+ return true;
129
+ } catch (error) {
130
+ if (error.code === DUPLICATE_KEY) return false;
131
+ throw error;
132
+ }
133
+ },
134
+ claim: async ({ workerId, leaseMs, limit }) => {
135
+ const { jobs } = await collections();
136
+ const at = await clock.now();
137
+ const taken = [];
138
+ for (let slot = 0; slot < limit; slot += 1) {
139
+ const claimed = await jobs.findOneAndUpdate({ $or: [{
140
+ status: "pending",
141
+ runAt: { $lte: at }
142
+ }, {
143
+ status: "running",
144
+ leaseUntil: { $lt: at }
145
+ }] }, [{ $set: { history: { $cond: [
146
+ { $eq: ["$status", "running"] },
147
+ lostAttempt(at),
148
+ "$history"
149
+ ] } } }, { $set: {
150
+ status: "running",
151
+ workerId,
152
+ leaseUntil: at + leaseMs,
153
+ updatedAt: at,
154
+ attempts: { $add: ["$attempts", 1] }
155
+ } }], {
156
+ sort: { runAt: 1 },
157
+ returnDocument: "after"
158
+ });
159
+ if (!claimed) break;
160
+ taken.push(toJob(claimed));
161
+ }
162
+ return taken;
163
+ },
164
+ heartbeat: async ({ jobIds, workerId, leaseMs }) => {
165
+ const { jobs } = await collections();
166
+ const at = await clock.now();
167
+ await jobs.updateMany({
168
+ _id: { $in: jobIds },
169
+ workerId,
170
+ status: "running"
171
+ }, { $set: { leaseUntil: at + leaseMs } });
172
+ return (await jobs.find({
173
+ _id: { $in: jobIds },
174
+ workerId,
175
+ status: "running",
176
+ cancelRequested: true
177
+ }).project({ _id: 1 }).toArray()).map((job) => job._id);
178
+ },
179
+ settle: async ({ jobId, workerId, status, delayMs, runId, error, attempt }) => {
180
+ const { jobs } = await collections();
181
+ const at = await clock.now();
182
+ const entry = {
183
+ attempt: { $add: [{ $size: "$history" }, 1] },
184
+ startedAt: "$updatedAt",
185
+ endedAt: at,
186
+ ...attempt
187
+ };
188
+ await jobs.updateOne({
189
+ _id: jobId,
190
+ workerId
191
+ }, [{ $set: {
192
+ ...attempt ? { history: { $concatArrays: ["$history", [entry]] } } : {},
193
+ status,
194
+ updatedAt: at,
195
+ leaseUntil: null,
196
+ error: error ?? null,
197
+ ...runId ? { runId } : {},
198
+ ...status === "pending" ? {
199
+ runAt: at + (delayMs ?? 0),
200
+ workerId: null,
201
+ ...attempt ? {} : { attempts: { $max: [0, { $subtract: ["$attempts", 1] }] } }
202
+ } : {},
203
+ expiresAt: TERMINAL.includes(status) ? expiryOf(at) : null
204
+ } }]);
205
+ },
206
+ dueSchedules: async (limit) => {
207
+ const { schedules } = await collections();
208
+ const at = await clock.now();
209
+ return (await schedules.find({
210
+ enabled: true,
211
+ nextRunAt: { $lte: at }
212
+ }).sort({ nextRunAt: 1 }).limit(limit).toArray()).map(toSchedule);
213
+ },
214
+ advanceSchedule: async ({ spaceId, actionId, from, to, missed }) => {
215
+ const { schedules } = await collections();
216
+ const at = await clock.now();
217
+ return (await schedules.updateOne({
218
+ _id: scheduleId(spaceId, actionId),
219
+ nextRunAt: from
220
+ }, {
221
+ $set: {
222
+ nextRunAt: to,
223
+ lastFireAt: from,
224
+ updatedAt: at
225
+ },
226
+ $inc: { missed }
227
+ })).matchedCount === 1;
228
+ },
229
+ putSchedules: async ({ spaceId, schedules: incoming }) => {
230
+ const { schedules } = await collections();
231
+ const at = await clock.now();
232
+ if (incoming.length > 0) await schedules.bulkWrite(incoming.map((schedule) => ({ updateOne: {
233
+ filter: { _id: scheduleId(spaceId, schedule.actionId) },
234
+ update: [{ $set: {
235
+ ...schedule,
236
+ timezone: schedule.timezone ?? null,
237
+ updatedAt: at,
238
+ missed: { $ifNull: ["$missed", 0] },
239
+ nextRunAt: { $cond: [
240
+ { $and: [
241
+ { $eq: ["$cron", schedule.cron] },
242
+ { $eq: [{ $ifNull: ["$timezone", null] }, schedule.timezone ?? null] },
243
+ { $eq: ["$environment", schedule.environment] },
244
+ { $ne: [{ $ifNull: ["$nextRunAt", null] }, null] }
245
+ ] },
246
+ "$nextRunAt",
247
+ schedule.nextRunAt
248
+ ] }
249
+ } }],
250
+ upsert: true
251
+ } })));
252
+ await schedules.deleteMany({
253
+ spaceId,
254
+ actionId: { $nin: incoming.map((schedule) => schedule.actionId) }
255
+ });
256
+ },
257
+ listJobs: async ({ spaceIds, actionId, status, limit, offset }) => {
258
+ const { jobs } = await collections();
259
+ const filter = {
260
+ spaceId: { $in: spaceIds },
261
+ ...actionId ? { actionId } : {},
262
+ ...status ? { status } : {}
263
+ };
264
+ const [found, total] = await Promise.all([jobs.find(filter).sort({
265
+ updatedAt: -1,
266
+ _id: -1
267
+ }).skip(offset).limit(limit).toArray(), jobs.countDocuments(filter)]);
268
+ return {
269
+ jobs: found.map(toJob),
270
+ total
271
+ };
272
+ },
273
+ getJob: async (spaceIds, jobId) => {
274
+ const { jobs } = await collections();
275
+ const job = await jobs.findOne({
276
+ _id: jobId,
277
+ spaceId: { $in: spaceIds }
278
+ });
279
+ return job ? toJob(job) : void 0;
280
+ },
281
+ listSchedules: async (spaceIds) => {
282
+ const { schedules } = await collections();
283
+ return (await schedules.find({ spaceId: { $in: spaceIds } }).sort({ nextRunAt: 1 }).toArray()).map(toSchedule);
284
+ },
285
+ requeue: async (spaceIds, jobId) => {
286
+ const { jobs } = await collections();
287
+ const at = await clock.now();
288
+ return (await jobs.updateOne({
289
+ _id: jobId,
290
+ spaceId: { $in: spaceIds },
291
+ status: { $in: TERMINAL }
292
+ }, {
293
+ $set: {
294
+ status: "pending",
295
+ runAt: at,
296
+ attempts: 0,
297
+ updatedAt: at,
298
+ cancelRequested: false
299
+ },
300
+ $unset: {
301
+ workerId: "",
302
+ leaseUntil: "",
303
+ error: "",
304
+ expiresAt: ""
305
+ }
306
+ })).matchedCount === 1;
307
+ },
308
+ cancel: async (spaceIds, jobId) => {
309
+ const { jobs } = await collections();
310
+ const at = await clock.now();
311
+ if ((await jobs.updateOne({
312
+ _id: jobId,
313
+ spaceId: { $in: spaceIds },
314
+ status: "pending"
315
+ }, {
316
+ $set: {
317
+ status: "cancelled",
318
+ updatedAt: at,
319
+ expiresAt: expiryOf(at)
320
+ },
321
+ $unset: { workerId: "" }
322
+ })).matchedCount === 1) return true;
323
+ return (await jobs.updateOne({
324
+ _id: jobId,
325
+ spaceId: { $in: spaceIds },
326
+ status: "running"
327
+ }, { $set: {
328
+ cancelRequested: true,
329
+ updatedAt: at
330
+ } })).matchedCount === 1;
331
+ }
332
+ };
333
+ };
334
+ //#endregion
335
+ export { createMongoJobQueue, resolveDb };
@@ -0,0 +1,15 @@
1
+ import { MongoDbSource } from './jobQueue';
2
+ import { ActionKvAdapter } from '../actions/types';
3
+ export type MongoKvOptions = {
4
+ db: MongoDbSource;
5
+ /** Default `action_kv`. */
6
+ collection?: string;
7
+ };
8
+ /**
9
+ * An {@link ActionKvAdapter} over a collection of a Mongo database the deployment owns.
10
+ *
11
+ * What the run guards' single-flight keys and the `kv` tasks' counters need from a store the replicas share: a TTL the
12
+ * store enforces and an increment nobody can race. Each operation is ONE document update, which Mongo applies
13
+ * atomically — so two replicas incrementing the same counter at once both count.
14
+ */
15
+ export declare const createMongoKv: ({ db, collection }: MongoKvOptions) => ActionKvAdapter;
@@ -0,0 +1,98 @@
1
+ import { resolveDb } from "./jobQueue.js";
2
+ //#region src/modules/mongo/kv.ts
3
+ /** Mongo's code for a unique index that refused a second document with the same key. */
4
+ var DUPLICATE_KEY = 11e3;
5
+ /**
6
+ * Whether a key is still alive, by the SERVER's clock.
7
+ *
8
+ * Mongo's TTL monitor sweeps once a minute, so a document past its lifetime can still be there to read. Expiry is
9
+ * decided here, against `$$NOW`, rather than by whether the sweeper has been round yet — the same answer Redis gives
10
+ * the instant a key lapses.
11
+ */
12
+ var LIVE = { $or: [{ $eq: [{ $ifNull: ["$expiresAt", null] }, null] }, { $gt: ["$expiresAt", "$$NOW"] }] };
13
+ var expiryIn = (ttlSeconds) => ({ $add: ["$$NOW", ttlSeconds * 1e3] });
14
+ /**
15
+ * An {@link ActionKvAdapter} over a collection of a Mongo database the deployment owns.
16
+ *
17
+ * What the run guards' single-flight keys and the `kv` tasks' counters need from a store the replicas share: a TTL the
18
+ * store enforces and an increment nobody can race. Each operation is ONE document update, which Mongo applies
19
+ * atomically — so two replicas incrementing the same counter at once both count.
20
+ */
21
+ var createMongoKv = ({ db, collection = "action_kv" }) => {
22
+ const keys = () => resolveDb(db).collection(collection);
23
+ let ready;
24
+ const store = async () => {
25
+ ready ??= keys().createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }).then(() => void 0).catch((error) => {
26
+ ready = void 0;
27
+ throw error;
28
+ });
29
+ await ready;
30
+ return keys();
31
+ };
32
+ /**
33
+ * An upsert two replicas race for can collide on `_id` before either has written; the server retries that itself
34
+ * from 4.2 on, and this covers the rest. Once is enough: the second attempt finds the document the first one made.
35
+ */
36
+ const upserting = async (write) => {
37
+ try {
38
+ return await write();
39
+ } catch (error) {
40
+ if (error.code !== DUPLICATE_KEY) throw error;
41
+ return write();
42
+ }
43
+ };
44
+ return {
45
+ get: async (key) => {
46
+ return (await (await store()).findOne({
47
+ _id: key,
48
+ $expr: LIVE
49
+ }))?.value;
50
+ },
51
+ set: async (key, value, ttlSeconds) => {
52
+ const keysNow = await store();
53
+ await upserting(() => keysNow.updateOne({ _id: key }, [{ $set: {
54
+ value,
55
+ expiresAt: ttlSeconds === void 0 ? null : expiryIn(ttlSeconds)
56
+ } }], { upsert: true }));
57
+ },
58
+ delete: async (key) => {
59
+ await (await store()).deleteOne({ _id: key });
60
+ },
61
+ increment: async (key, amount) => {
62
+ const keysNow = await store();
63
+ const updated = await upserting(() => keysNow.findOneAndUpdate({ _id: key }, [
64
+ { $set: { live: LIVE } },
65
+ { $set: {
66
+ value: { $toString: { $add: [{ $cond: [
67
+ "$live",
68
+ { $convert: {
69
+ input: "$value",
70
+ to: "double",
71
+ onError: 0,
72
+ onNull: 0
73
+ } },
74
+ 0
75
+ ] }, amount] } },
76
+ expiresAt: { $cond: [
77
+ "$live",
78
+ { $ifNull: ["$expiresAt", null] },
79
+ null
80
+ ] }
81
+ } },
82
+ { $unset: "live" }
83
+ ], {
84
+ upsert: true,
85
+ returnDocument: "after"
86
+ }));
87
+ return Number(updated?.value ?? amount);
88
+ },
89
+ expire: async (key, ttlSeconds) => {
90
+ await (await store()).updateOne({
91
+ _id: key,
92
+ $expr: LIVE
93
+ }, [{ $set: { expiresAt: expiryIn(ttlSeconds) } }]);
94
+ }
95
+ };
96
+ };
97
+ //#endregion
98
+ export { createMongoKv };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export { createMysqlJobQueue } from './jobQueue';
2
+ export { createMysqlKv } from './kv';
3
+ export { mysqlJobSchemaStatements } from './schema';
4
+ export type { MysqlJobQueue, MysqlJobQueueOptions } from './jobQueue';
5
+ export type { MysqlKvOptions } from './kv';
@@ -0,0 +1,40 @@
1
+ import { ActionJobQueue } from '@plitzi/sdk-shared';
2
+ import { Pool } from 'mysql2/promise';
3
+ export type MysqlJobQueueOptions = {
4
+ /** A pool the deployment opened. The helper borrows connections from it and never ends it. */
5
+ pool: Pool;
6
+ /** Prefix for the three tables — `action_jobs`, `action_schedules`, `action_kv`. Default none. */
7
+ tablePrefix?: string;
8
+ /**
9
+ * Create the tables on first use (`CREATE TABLE IF NOT EXISTS`). Default true; false for a deployment that runs
10
+ * {@link mysqlJobSchemaStatements} through its own migrations.
11
+ */
12
+ createTables?: boolean;
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 MysqlJobQueue = 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 tables in a MySQL database the deployment owns. Tested on MySQL 8; nothing in it
22
+ * needs more than 5.7.
23
+ *
24
+ * The helper for a deployment that already runs MySQL: it borrows connections from the pool it is handed, and creates
25
+ * its three tables on first use unless told not to. Every replica pointed at the same database shares the jobs and the
26
+ * schedules.
27
+ *
28
+ * ```ts
29
+ * import mysql from 'mysql2/promise';
30
+ * import { createMysqlJobQueue, createMysqlKv } from '@plitzi/sdk-server/mysql';
31
+ *
32
+ * const pool = mysql.createPool(process.env.DATABASE_URL);
33
+ * createServer({ action: { lookups, kv: createMysqlKv({ pool }), jobs: { queue: createMysqlJobQueue({ pool }) } } });
34
+ * ```
35
+ *
36
+ * The contract's rules, as MySQL keeps them: every instant is the SERVER's (`NOW(3)`), `enqueue` is idempotent by the
37
+ * primary key, `claim` takes each job with a single-row compare-and-set — so two workers never take the same one —
38
+ * and reaps a lapsed lease in the same write, and a schedule advances by compare-and-set too.
39
+ */
40
+ export declare const createMysqlJobQueue: ({ pool, tablePrefix, createTables, retentionDays }: MysqlJobQueueOptions) => MysqlJobQueue;