@byline/db-mysql 4.14.1 → 4.16.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.
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  import { eq, relations, sql } from 'drizzle-orm';
9
- import { bigint, boolean, date, datetime, decimal, float, foreignKey, index, int, json, mysqlTable, mysqlView, primaryKey, text, time, unique, varchar, } from 'drizzle-orm/mysql-core';
9
+ import { bigint, boolean, check, date, datetime, decimal, float, foreignKey, index, int, json, mysqlTable, mysqlView, primaryKey, text, time, unique, varchar, } from 'drizzle-orm/mysql-core';
10
10
  import { createdAt, timestamps, uuidChar, varcharByteSorted, varcharCaseSensitive, } from './common.js';
11
11
  // Every foreign key below is declared with the table-level `foreignKey()`
12
12
  // builder and an explicit, short `fk_<table>_<column>` name, rather than
@@ -115,6 +115,55 @@ export const documentVersions = mysqlTable('byline_document_versions', {
115
115
  // Ensure logical document belongs to only one collection
116
116
  index('idx_documents_document_collection').on(table.document_id, table.collection_id),
117
117
  ]);
118
+ // Scheduled publication — one active intent row per logical document.
119
+ // This is document lifecycle metadata, not authored/versioned content. The
120
+ // target version is pinned until a later content write suspends the row for
121
+ // explicit re-confirmation. See specs/2026-08-22-scheduled-publish.md.
122
+ export const documentPublishSchedules = mysqlTable('byline_document_publish_schedules', {
123
+ document_id: uuidChar('document_id').primaryKey(),
124
+ collection_id: uuidChar('collection_id').notNull(),
125
+ target_version_id: uuidChar('target_version_id').notNull(),
126
+ publish_at: datetime('publish_at', { fsp: 6 }).notNull(),
127
+ state: varchar('state', { length: 32 }).notNull().default('armed'),
128
+ suspended_at: datetime('suspended_at', { fsp: 6 }),
129
+ suspended_reason: varchar('suspended_reason', { length: 32 }),
130
+ // Historical and current authorizers are deliberately not foreign keys:
131
+ // deleting an admin account must not revoke an already-authorized schedule.
132
+ scheduled_by: uuidChar('scheduled_by'),
133
+ last_authorized_by: uuidChar('last_authorized_by'),
134
+ last_authorized_at: datetime('last_authorized_at', { fsp: 6 }).notNull(),
135
+ scheduled_at: datetime('scheduled_at', { fsp: 6 }).notNull().default(sql `CURRENT_TIMESTAMP(6)`),
136
+ updated_at: datetime('updated_at', { fsp: 6 }).notNull().default(sql `CURRENT_TIMESTAMP(6)`),
137
+ execution_token: uuidChar('execution_token'),
138
+ execution_expires_at: datetime('execution_expires_at', { fsp: 6 }),
139
+ last_attempt_at: datetime('last_attempt_at', { fsp: 6 }),
140
+ next_attempt_at: datetime('next_attempt_at', { fsp: 6 }).notNull(),
141
+ attempt_count: int('attempt_count').notNull().default(0),
142
+ // Sanitized by the storage command and capped at 2 KiB; never stores a
143
+ // stack or document content.
144
+ last_error: text('last_error'),
145
+ }, (table) => [
146
+ foreignKey({
147
+ name: 'fk_publish_schedules_document',
148
+ columns: [table.document_id],
149
+ foreignColumns: [documents.id],
150
+ }).onDelete('cascade'),
151
+ foreignKey({
152
+ name: 'fk_publish_schedules_collection',
153
+ columns: [table.collection_id],
154
+ foreignColumns: [collections.id],
155
+ }).onDelete('cascade'),
156
+ foreignKey({
157
+ name: 'fk_publish_schedules_target_version',
158
+ columns: [table.target_version_id],
159
+ foreignColumns: [documentVersions.id],
160
+ }).onDelete('cascade'),
161
+ check('check_publish_schedules_state', sql `${table.state} IN ('armed', 'needs_reconfirm')`),
162
+ check('check_publish_schedules_suspended_reason', sql `${table.suspended_reason} IS NULL OR ${table.suspended_reason} = 'content_edited'`),
163
+ // MySQL has no partial indexes, so state is the leading discriminator.
164
+ index('idx_document_publish_schedules_due').on(table.state, table.next_attempt_at, table.publish_at),
165
+ index('idx_document_publish_schedules_execution_expiry').on(table.execution_expires_at),
166
+ ]);
118
167
  // Document paths — one row per (logical document, content locale).
119
168
  // Promotes `path` out of the version row so per-collection uniqueness can
120
169
  // be enforced at the DB layer without colliding with the sticky
@@ -894,6 +943,37 @@ export const auditLog = mysqlTable('byline_audit_log', {
894
943
  index('idx_audit_log_action').on(table.action, table.id),
895
944
  ]);
896
945
  // ---------------------------------------------------------------------------
946
+ // Recurring-task scheduler — byline_recurring_tasks
947
+ // ---------------------------------------------------------------------------
948
+ // Recurring-task scheduler (specs/2026-08-22-scheduler.md).
949
+ // Code-registered definitions are authoritative; rows for definitions removed
950
+ // from code are retained as dormant history and never executed.
951
+ export const recurringTasks = mysqlTable('byline_recurring_tasks', {
952
+ name: varchar('name', { length: 255 }).primaryKey(),
953
+ // bigint: MySQL INT tops out at ~24.9 days in milliseconds, and a 30-day
954
+ // interval is a legitimate configuration. `mode: 'number'` is safe because
955
+ // boot-time validation guarantees every millisecond value is a JS safe integer.
956
+ interval_ms: bigint('interval_ms', { mode: 'number' }).notNull(),
957
+ next_run_at: datetime('next_run_at', { fsp: 6 }).notNull(),
958
+ // A token unique to one claim — not a stable machine id. Every health and
959
+ // schedule write is conditional on it, so a slow runner whose lease expired
960
+ // cannot overwrite a newer run.
961
+ lease_token: uuidChar('lease_token'),
962
+ // Bounded, non-secret diagnostic label (machine id + pid). Correctness never
963
+ // depends on owner uniqueness.
964
+ lease_owner: varchar('lease_owner', { length: 255 }),
965
+ lease_expires_at: datetime('lease_expires_at', { fsp: 6 }),
966
+ last_started_at: datetime('last_started_at', { fsp: 6 }),
967
+ last_succeeded_at: datetime('last_succeeded_at', { fsp: 6 }),
968
+ last_failed_at: datetime('last_failed_at', { fsp: 6 }),
969
+ last_duration_ms: bigint('last_duration_ms', { mode: 'number' }),
970
+ consecutive_failures: int('consecutive_failures').notNull().default(0),
971
+ last_status: varchar('last_status', { length: 32 }).notNull().default('never_run'),
972
+ // Sanitized message, capped at 2 KiB by the store. Full stacks go to the logger.
973
+ last_error: text('last_error'),
974
+ ...timestamps,
975
+ });
976
+ // ---------------------------------------------------------------------------
897
977
  // Auth schema — byline_admin_users, byline_admin_roles, etc.
898
978
  // See ./auth.ts for definitions and rationale.
899
979
  // ---------------------------------------------------------------------------
@@ -418,3 +418,48 @@ describe('schema pins — document-paths case-sensitive collation (project-owner
418
418
  expect(pathColumn?.getSQLType()).toBe('varchar(255) COLLATE utf8mb4_bin');
419
419
  });
420
420
  });
421
+ describe('schema pins — scheduled publication', () => {
422
+ const cfg = getTableConfig(coreSchema.documentPublishSchedules);
423
+ it('pins one row per document and exactly the three lifecycle ownership foreign keys', () => {
424
+ const documentId = cfg.columns.find((column) => column.name === 'document_id');
425
+ expect(documentId?.primary).toBe(true);
426
+ const references = cfg.foreignKeys.map((foreignKey) => ({
427
+ name: foreignKey.getName(),
428
+ local: foreignKey.reference().columns.map((column) => column.name),
429
+ onDelete: foreignKey.onDelete,
430
+ }));
431
+ expect(references).toEqual([
432
+ {
433
+ name: 'fk_publish_schedules_document',
434
+ local: ['document_id'],
435
+ onDelete: 'cascade',
436
+ },
437
+ {
438
+ name: 'fk_publish_schedules_collection',
439
+ local: ['collection_id'],
440
+ onDelete: 'cascade',
441
+ },
442
+ {
443
+ name: 'fk_publish_schedules_target_version',
444
+ local: ['target_version_id'],
445
+ onDelete: 'cascade',
446
+ },
447
+ ]);
448
+ });
449
+ it('pins the bounded state and suspension-reason constraints', () => {
450
+ expect(cfg.checks.map((constraint) => constraint.name).sort()).toEqual([
451
+ 'check_publish_schedules_state',
452
+ 'check_publish_schedules_suspended_reason',
453
+ ]);
454
+ });
455
+ it('pins state as the MySQL due-index discriminator and execution-expiry recovery', () => {
456
+ const due = cfg.indexes.find((candidate) => candidate.config.name === 'idx_document_publish_schedules_due');
457
+ expect(due?.config.columns.map((column) => ('name' in column ? column.name : null))).toEqual([
458
+ 'state',
459
+ 'next_attempt_at',
460
+ 'publish_at',
461
+ ]);
462
+ const expiry = cfg.indexes.find((candidate) => candidate.config.name === 'idx_document_publish_schedules_execution_expiry');
463
+ expect(expiry?.config.columns.map((column) => ('name' in column ? column.name : null))).toEqual(['execution_expires_at']);
464
+ });
465
+ });
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { DBManagerImpl, TXManagerImpl } from './lib/db-manager.js';
13
13
  import { createAuditCommands } from './modules/audit/audit-commands.js';
14
14
  import { createAuditQueries } from './modules/audit/audit-queries.js';
15
15
  import { createCounterCommands } from './modules/counters/counters-commands.js';
16
+ import { createSchedulerStore } from './modules/scheduler/scheduler-store.js';
16
17
  import { classifyError } from './modules/storage/classify-error.js';
17
18
  import { createCommandBuilders } from './modules/storage/storage-commands.js';
18
19
  import { createQueryBuilders } from './modules/storage/storage-queries.js';
@@ -85,6 +86,7 @@ export const mysqlAdapter = ({ connectionString, collections, defaultContentLoca
85
86
  const counterCommands = createCounterCommands(pool);
86
87
  const auditCommands = createAuditCommands(dbManager);
87
88
  const auditQueries = createAuditQueries(db);
89
+ const schedulerStore = createSchedulerStore(db);
88
90
  // Boot check: run lazily on the pool's first physical connection rather
89
91
  // than eagerly here, because `mysqlAdapter` is synchronous (mirroring
90
92
  // `pgAdapter`) and cannot await a round trip before returning. mysql2
@@ -144,6 +146,7 @@ export const mysqlAdapter = ({ connectionString, collections, defaultContentLoca
144
146
  audit: auditQueries,
145
147
  },
146
148
  withTransaction: (fn) => txManager.withTransaction(fn),
149
+ scheduler: schedulerStore,
147
150
  classifyError,
148
151
  drizzle: db,
149
152
  pool,
@@ -0,0 +1,13 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { ISchedulerStore } from '@byline/core';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ type DatabaseConnection = MySql2Database<typeof schema>;
12
+ export declare function createSchedulerStore(db: DatabaseConnection): ISchedulerStore;
13
+ export {};
@@ -0,0 +1,237 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * MySQL implementation of `ISchedulerStore` (specs/2026-08-22-scheduler.md).
10
+ * Every due and lease comparison uses database UTC time, and every write after
11
+ * a claim is fenced by the claim's token.
12
+ *
13
+ * MySQL has no `UPDATE ... RETURNING`, so `claim()` locks the eligible row,
14
+ * updates it, and returns the pre-claim schedule from one READ COMMITTED
15
+ * transaction. The transaction is deliberately store-owned rather than ambient:
16
+ * scheduler claims must not inherit a caller's longer transaction boundary.
17
+ */
18
+ import { randomUUID } from 'node:crypto';
19
+ import { MAX_BACKOFF_MS } from '@byline/core/scheduler';
20
+ import { sql } from 'drizzle-orm';
21
+ import { affectedRowCount, toDate } from '../storage/storage-utils.js';
22
+ /** Doc comment on `ISchedulerStore.fail`: 2048 characters, never a stack trace. */
23
+ const LAST_ERROR_MAX_LENGTH = 2048;
24
+ /** Doc comment on `ISchedulerStore.health`: `lease_owner` capped at 255 characters. */
25
+ const LEASE_OWNER_MAX_LENGTH = 255;
26
+ const MAX_BACKOFF_SECONDS = MAX_BACKOFF_MS / 1000;
27
+ function requiredDate(value, context) {
28
+ const date = toDate(value, context);
29
+ if (date === null) {
30
+ throw new Error(`scheduler store: ${context} unexpectedly returned null`);
31
+ }
32
+ return date;
33
+ }
34
+ class SchedulerStore {
35
+ db;
36
+ constructor(db) {
37
+ this.db = db;
38
+ }
39
+ /**
40
+ * One conflict-tolerant upsert per definition. `interval_ms` always follows
41
+ * code, while an unleased `next_run_at` is only pulled earlier when the new
42
+ * interval would otherwise leave the task waiting too long.
43
+ */
44
+ async reconcile(tasks) {
45
+ for (const task of tasks) {
46
+ await this.db.execute(sql `
47
+ INSERT INTO byline_recurring_tasks (name, interval_ms, next_run_at, last_status)
48
+ VALUES (
49
+ ${task.name},
50
+ ${task.intervalMs},
51
+ TIMESTAMPADD(MICROSECOND, ${task.intervalMs} * 1000, CURRENT_TIMESTAMP(6)),
52
+ 'never_run'
53
+ )
54
+ ON DUPLICATE KEY UPDATE
55
+ next_run_at = CASE
56
+ WHEN lease_token IS NULL
57
+ AND next_run_at > TIMESTAMPADD(
58
+ MICROSECOND,
59
+ ${task.intervalMs} * 1000,
60
+ CURRENT_TIMESTAMP(6)
61
+ )
62
+ THEN TIMESTAMPADD(MICROSECOND, ${task.intervalMs} * 1000, CURRENT_TIMESTAMP(6))
63
+ ELSE next_run_at
64
+ END,
65
+ interval_ms = ${task.intervalMs},
66
+ updated_at = CURRENT_TIMESTAMP(6)
67
+ `);
68
+ }
69
+ }
70
+ /**
71
+ * Atomic claim through a locking read and fenced update on one physical
72
+ * connection. Under READ COMMITTED, contenders blocked on `FOR UPDATE`
73
+ * re-check the eligibility predicate after the winner commits and return
74
+ * null instead of observing the stale due row.
75
+ */
76
+ async claim(params) {
77
+ return this.db.transaction(async (tx) => {
78
+ const selected = (await tx.execute(sql `
79
+ SELECT
80
+ name,
81
+ next_run_at AS scheduled_for,
82
+ CURRENT_TIMESTAMP(6) AS database_now,
83
+ (lease_expires_at IS NOT NULL) AS recovered_expired_lease
84
+ FROM byline_recurring_tasks
85
+ WHERE name = ${params.name}
86
+ AND next_run_at <= CURRENT_TIMESTAMP(6)
87
+ AND (lease_expires_at IS NULL OR lease_expires_at <= CURRENT_TIMESTAMP(6))
88
+ FOR UPDATE
89
+ `));
90
+ const row = selected[0][0];
91
+ if (row === undefined)
92
+ return null;
93
+ const leaseToken = randomUUID();
94
+ const result = await tx.execute(sql `
95
+ UPDATE byline_recurring_tasks SET
96
+ lease_token = ${leaseToken},
97
+ lease_owner = ${params.owner.slice(0, LEASE_OWNER_MAX_LENGTH)},
98
+ lease_expires_at = TIMESTAMPADD(
99
+ MICROSECOND,
100
+ ${params.leaseMs} * 1000,
101
+ CURRENT_TIMESTAMP(6)
102
+ ),
103
+ last_started_at = CURRENT_TIMESTAMP(6),
104
+ last_status = 'running',
105
+ updated_at = CURRENT_TIMESTAMP(6)
106
+ WHERE name = ${params.name}
107
+ `);
108
+ if (affectedRowCount(result) !== 1) {
109
+ throw new Error(`scheduler store: locked claim row '${params.name}' disappeared`);
110
+ }
111
+ return {
112
+ name: row.name,
113
+ leaseToken,
114
+ scheduledFor: requiredDate(row.scheduled_for, 'scheduled_for'),
115
+ databaseNow: requiredDate(row.database_now, 'database_now'),
116
+ recoveredExpiredLease: Boolean(row.recovered_expired_lease),
117
+ };
118
+ }, { isolationLevel: 'read committed' });
119
+ }
120
+ /**
121
+ * Do not add an expiry predicate: expiry makes a row claimable but does not
122
+ * invalidate the current token until another claimant replaces it.
123
+ */
124
+ async renew(params) {
125
+ const result = await this.db.execute(sql `
126
+ UPDATE byline_recurring_tasks SET
127
+ lease_expires_at = TIMESTAMPADD(
128
+ MICROSECOND,
129
+ ${params.leaseMs} * 1000,
130
+ CURRENT_TIMESTAMP(6)
131
+ ),
132
+ updated_at = CURRENT_TIMESTAMP(6)
133
+ WHERE name = ${params.name} AND lease_token = ${params.leaseToken}
134
+ `);
135
+ return affectedRowCount(result) === 1;
136
+ }
137
+ /**
138
+ * Complete a token-matched run and schedule from the persisted cadence.
139
+ * `interval_ms` is read but not assigned here, so assignment order is
140
+ * currently irrelevant. If a future change adds it to this SET list, keep
141
+ * that assignment after `next_run_at` so the persisted pre-update cadence is
142
+ * retained; see `fail()` for MySQL's left-to-right assignment rule.
143
+ */
144
+ async complete(params) {
145
+ const nextRunAt = params.workRemaining
146
+ ? sql `CURRENT_TIMESTAMP(6)`
147
+ : sql `TIMESTAMPADD(MICROSECOND, interval_ms * 1000, CURRENT_TIMESTAMP(6))`;
148
+ const result = await this.db.execute(sql `
149
+ UPDATE byline_recurring_tasks SET
150
+ last_succeeded_at = CURRENT_TIMESTAMP(6),
151
+ last_status = 'succeeded',
152
+ last_duration_ms = ${params.durationMs},
153
+ consecutive_failures = 0,
154
+ last_error = NULL,
155
+ lease_token = NULL,
156
+ lease_owner = NULL,
157
+ lease_expires_at = NULL,
158
+ next_run_at = ${nextRunAt},
159
+ updated_at = CURRENT_TIMESTAMP(6)
160
+ WHERE name = ${params.name} AND lease_token = ${params.leaseToken}
161
+ `);
162
+ return affectedRowCount(result) === 1;
163
+ }
164
+ /**
165
+ * Record a token-matched failure with 1, 2, 4, 8, then 15 minute backoff.
166
+ * MySQL evaluates single-table UPDATE assignments from left to right, so
167
+ * `next_run_at` intentionally reads the old `consecutive_failures` before
168
+ * the following assignment increments it.
169
+ */
170
+ async fail(params) {
171
+ const result = await this.db.execute(sql `
172
+ UPDATE byline_recurring_tasks SET
173
+ last_failed_at = CURRENT_TIMESTAMP(6),
174
+ last_status = 'failed',
175
+ last_duration_ms = ${params.durationMs},
176
+ last_error = ${params.error.slice(0, LAST_ERROR_MAX_LENGTH)},
177
+ lease_token = NULL,
178
+ lease_owner = NULL,
179
+ lease_expires_at = NULL,
180
+ next_run_at = TIMESTAMPADD(
181
+ SECOND,
182
+ LEAST(
183
+ 60 * CAST(POWER(2, LEAST(consecutive_failures, 4)) AS UNSIGNED),
184
+ ${MAX_BACKOFF_SECONDS}
185
+ ),
186
+ CURRENT_TIMESTAMP(6)
187
+ ),
188
+ consecutive_failures = consecutive_failures + 1,
189
+ updated_at = CURRENT_TIMESTAMP(6)
190
+ WHERE name = ${params.name} AND lease_token = ${params.leaseToken}
191
+ `);
192
+ return affectedRowCount(result) === 1;
193
+ }
194
+ /** Return selected health rows, or every row when names is omitted. */
195
+ async health(names) {
196
+ if (names?.length === 0)
197
+ return [];
198
+ const whereClause = names === undefined
199
+ ? sql ``
200
+ : sql `WHERE name IN (${sql.join(names.map((name) => sql `${name}`), sql `, `)})`;
201
+ const result = (await this.db.execute(sql `
202
+ SELECT
203
+ name,
204
+ interval_ms,
205
+ next_run_at,
206
+ last_status,
207
+ last_started_at,
208
+ last_succeeded_at,
209
+ last_failed_at,
210
+ last_duration_ms,
211
+ consecutive_failures,
212
+ last_error,
213
+ (lease_expires_at IS NOT NULL AND lease_expires_at <= CURRENT_TIMESTAMP(6)) AS lease_expired,
214
+ CURRENT_TIMESTAMP(6) AS database_now
215
+ FROM byline_recurring_tasks
216
+ ${whereClause}
217
+ ORDER BY name
218
+ `));
219
+ return result[0].map((row) => ({
220
+ name: row.name,
221
+ intervalMs: Number(row.interval_ms),
222
+ nextRunAt: requiredDate(row.next_run_at, 'next_run_at'),
223
+ lastStatus: row.last_status,
224
+ lastStartedAt: toDate(row.last_started_at, 'last_started_at'),
225
+ lastSucceededAt: toDate(row.last_succeeded_at, 'last_succeeded_at'),
226
+ lastFailedAt: toDate(row.last_failed_at, 'last_failed_at'),
227
+ lastDurationMs: row.last_duration_ms === null ? null : Number(row.last_duration_ms),
228
+ consecutiveFailures: Number(row.consecutive_failures),
229
+ lastError: row.last_error,
230
+ leaseExpired: Boolean(row.lease_expired),
231
+ databaseNow: requiredDate(row.database_now, 'database_now'),
232
+ }));
233
+ }
234
+ }
235
+ export function createSchedulerStore(db) {
236
+ return new SchedulerStore(db);
237
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ import type { ClaimedDocumentPublishSchedule, ConfirmDocumentPublishScheduleResult, DocumentPublishSchedule, DocumentPublishSchedulePage, DocumentPublishScheduleState, IDocumentPublishScheduleCommands, IDocumentPublishScheduleQueries, ScheduleDocumentPublishResult, SuspendDocumentPublishScheduleResult } from '@byline/core';
9
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
10
+ import type * as schema from '../../database/schema/index.js';
11
+ import type { DBManager } from '../../lib/db-manager.js';
12
+ type DatabaseConnection = MySql2Database<typeof schema>;
13
+ /** MySQL scheduled-publication writes on the ambient document transaction. */
14
+ export declare class DocumentPublishScheduleCommands implements IDocumentPublishScheduleCommands {
15
+ private dbManager;
16
+ constructor(dbManager: DBManager);
17
+ private get db();
18
+ private lockCurrentVersion;
19
+ private lockSchedule;
20
+ private readSchedule;
21
+ schedule(params: {
22
+ documentId: string;
23
+ collectionId: string;
24
+ expectedVersionId: string;
25
+ publishAt: Date;
26
+ actorId: string | null;
27
+ }): Promise<ScheduleDocumentPublishResult>;
28
+ confirm(params: {
29
+ documentId: string;
30
+ collectionId: string;
31
+ expectedVersionId: string;
32
+ actorId: string | null;
33
+ }): Promise<ConfirmDocumentPublishScheduleResult>;
34
+ cancel(params: {
35
+ documentId: string;
36
+ collectionId: string;
37
+ }): Promise<DocumentPublishSchedule | null>;
38
+ suspendForContentEdit(params: {
39
+ documentId: string;
40
+ collectionId: string;
41
+ }): Promise<SuspendDocumentPublishScheduleResult>;
42
+ claimDue(params: {
43
+ batchSize: number;
44
+ leaseMs: number;
45
+ }): Promise<ClaimedDocumentPublishSchedule[]>;
46
+ lockClaim(params: {
47
+ documentId: string;
48
+ executionToken: string;
49
+ }): Promise<DocumentPublishSchedule | null>;
50
+ deleteClaim(params: {
51
+ documentId: string;
52
+ executionToken: string;
53
+ }): Promise<boolean>;
54
+ suspendClaimForContentEdit(params: {
55
+ documentId: string;
56
+ executionToken: string;
57
+ }): Promise<boolean>;
58
+ /**
59
+ * `attempt_count` is read but not assigned, so MySQL's left-to-right SET
60
+ * evaluation cannot shift the backoff sequence. If it is ever assigned in
61
+ * this statement, keep `next_attempt_at` before that assignment.
62
+ */
63
+ releaseClaim(params: {
64
+ documentId: string;
65
+ executionToken: string;
66
+ error: string;
67
+ }): Promise<boolean>;
68
+ }
69
+ /** MySQL scheduled-publication reads. */
70
+ export declare class DocumentPublishScheduleQueries implements IDocumentPublishScheduleQueries {
71
+ private db;
72
+ constructor(db: DatabaseConnection);
73
+ get(params: {
74
+ documentId: string;
75
+ collectionId: string;
76
+ }): Promise<DocumentPublishSchedule | null>;
77
+ list(params: {
78
+ collectionIds: readonly string[];
79
+ states?: readonly DocumentPublishScheduleState[];
80
+ lastAuthorizedBy?: string;
81
+ page: number;
82
+ pageSize: number;
83
+ }): Promise<DocumentPublishSchedulePage>;
84
+ }
85
+ export {};