@byline/core 4.14.1 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/@types/db-types.d.ts +222 -0
- package/dist/@types/site-config.d.ts +24 -0
- package/dist/codegen/index.test.node.js +1 -1
- package/dist/core.d.ts +8 -0
- package/dist/core.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -11
- package/dist/scheduler/define-recurring-task.d.ts +19 -0
- package/dist/scheduler/define-recurring-task.js +20 -0
- package/dist/scheduler/index.d.ts +19 -0
- package/dist/scheduler/index.js +18 -0
- package/dist/scheduler/run-due-tasks.d.ts +39 -0
- package/dist/scheduler/run-due-tasks.js +324 -0
- package/dist/scheduler/run-due-tasks.test.node.d.ts +8 -0
- package/dist/scheduler/run-due-tasks.test.node.js +396 -0
- package/dist/scheduler/scheduled-publication-constants.d.ts +11 -0
- package/dist/scheduler/scheduled-publication-constants.js +11 -0
- package/dist/scheduler/scheduled-publication.d.ts +31 -0
- package/dist/scheduler/scheduled-publication.js +198 -0
- package/dist/scheduler/scheduled-publication.test.node.d.ts +8 -0
- package/dist/scheduler/scheduled-publication.test.node.js +109 -0
- package/dist/scheduler/scheduler-boot.test.node.d.ts +8 -0
- package/dist/scheduler/scheduler-boot.test.node.js +103 -0
- package/dist/scheduler/ticker.d.ts +34 -0
- package/dist/scheduler/ticker.js +144 -0
- package/dist/scheduler/ticker.test.node.d.ts +8 -0
- package/dist/scheduler/ticker.test.node.js +249 -0
- package/dist/scheduler/types.d.ts +241 -0
- package/dist/scheduler/types.js +8 -0
- package/dist/scheduler/validate-scheduler-config.d.ts +19 -0
- package/dist/scheduler/validate-scheduler-config.js +25 -0
- package/dist/scheduler/validate-scheduler-config.test.node.d.ts +8 -0
- package/dist/scheduler/validate-scheduler-config.test.node.js +38 -0
- package/dist/scheduler/validate-tasks.d.ts +14 -0
- package/dist/scheduler/validate-tasks.js +40 -0
- package/dist/scheduler/validate-tasks.test.node.d.ts +8 -0
- package/dist/scheduler/validate-tasks.test.node.js +49 -0
- package/dist/services/collection-bootstrap.test.node.js +2 -0
- package/dist/services/discover-counter-groups.test.node.js +2 -0
- package/dist/services/document-lifecycle/audit.d.ts +6 -0
- package/dist/services/document-lifecycle/audit.js +6 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +15 -10
- package/dist/services/document-lifecycle/delete-locale.js +18 -10
- package/dist/services/document-lifecycle/delete.js +8 -0
- package/dist/services/document-lifecycle/index.d.ts +1 -0
- package/dist/services/document-lifecycle/index.js +1 -0
- package/dist/services/document-lifecycle/publish-schedule-consistency.d.ts +36 -0
- package/dist/services/document-lifecycle/publish-schedule-consistency.js +80 -0
- package/dist/services/document-lifecycle/restore.js +15 -10
- package/dist/services/document-lifecycle/scheduled-publish.d.ts +51 -0
- package/dist/services/document-lifecycle/scheduled-publish.js +351 -0
- package/dist/services/document-lifecycle/status-transition.d.ts +42 -0
- package/dist/services/document-lifecycle/status-transition.js +41 -0
- package/dist/services/document-lifecycle/status-transition.test.node.d.ts +8 -0
- package/dist/services/document-lifecycle/status-transition.test.node.js +145 -0
- package/dist/services/document-lifecycle/status.js +30 -23
- package/dist/services/document-lifecycle/tree.test.node.js +3 -0
- package/dist/services/document-lifecycle/update.js +35 -30
- package/dist/services/document-lifecycle.test.node.js +262 -2
- package/dist/services/field-upload.test.node.js +2 -0
- package/dist/services/populate.test.node.js +2 -0
- package/package.json +7 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RequestContext } from '@byline/auth';
|
|
2
2
|
import type { CollectionDefinition } from '@byline/core';
|
|
3
3
|
import type { DbErrorClassification } from '../lib/errors.js';
|
|
4
|
+
import type { ISchedulerStore } from '../scheduler/types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Read mode for document queries.
|
|
6
7
|
*
|
|
@@ -250,6 +251,13 @@ export interface IDbAdapter {
|
|
|
250
251
|
* Canonical adapters (db-postgres, db-mysql) implement it.
|
|
251
252
|
*/
|
|
252
253
|
classifyError?(err: unknown): DbErrorClassification;
|
|
254
|
+
/**
|
|
255
|
+
* Optional recurring-task scheduler storage. Present on the canonical
|
|
256
|
+
* adapters; absent adapters simply cannot run recurring tasks, which
|
|
257
|
+
* `initBylineCore()` reports at boot rather than failing silently later.
|
|
258
|
+
* See specs/2026-08-22-scheduler.md.
|
|
259
|
+
*/
|
|
260
|
+
scheduler?: ISchedulerStore;
|
|
253
261
|
}
|
|
254
262
|
/**
|
|
255
263
|
* The realm of the actor that performed an audited change. `'admin'` for
|
|
@@ -482,7 +490,218 @@ export interface TreeDeleteMutationResult {
|
|
|
482
490
|
removed: TreeMutationResult;
|
|
483
491
|
promoted: TreePromotionChange[];
|
|
484
492
|
}
|
|
493
|
+
/** Durable state of one document-grain scheduled-publication intent. */
|
|
494
|
+
export type DocumentPublishScheduleState = 'armed' | 'needs_reconfirm';
|
|
495
|
+
/** Why an armed schedule stopped being executable. */
|
|
496
|
+
type DocumentPublishScheduleSuspendedReason = 'content_edited';
|
|
497
|
+
/**
|
|
498
|
+
* Adapter-neutral representation of `byline_document_publish_schedules`.
|
|
499
|
+
*
|
|
500
|
+
* Dates are always real `Date` instances. Canonical adapters must normalize
|
|
501
|
+
* raw driver strings before returning this shape; callers must never need to
|
|
502
|
+
* know whether the backing engine uses `timestamptz` or `datetime(6)`.
|
|
503
|
+
*/
|
|
504
|
+
export interface DocumentPublishSchedule {
|
|
505
|
+
documentId: string;
|
|
506
|
+
collectionId: string;
|
|
507
|
+
targetVersionId: string;
|
|
508
|
+
publishAt: Date;
|
|
509
|
+
state: DocumentPublishScheduleState;
|
|
510
|
+
suspendedAt: Date | null;
|
|
511
|
+
suspendedReason: DocumentPublishScheduleSuspendedReason | null;
|
|
512
|
+
/** Historical creator. Preserved across reschedule and re-confirmation. */
|
|
513
|
+
scheduledBy: string | null;
|
|
514
|
+
/** Actor whose authorization the currently armed intent rests on. */
|
|
515
|
+
lastAuthorizedBy: string | null;
|
|
516
|
+
lastAuthorizedAt: Date;
|
|
517
|
+
scheduledAt: Date;
|
|
518
|
+
updatedAt: Date;
|
|
519
|
+
executionToken: string | null;
|
|
520
|
+
executionExpiresAt: Date | null;
|
|
521
|
+
lastAttemptAt: Date | null;
|
|
522
|
+
nextAttemptAt: Date;
|
|
523
|
+
attemptCount: number;
|
|
524
|
+
/** Sanitized, stack-free, and capped at 2 KiB by the adapter. */
|
|
525
|
+
lastError: string | null;
|
|
526
|
+
}
|
|
527
|
+
/** A due schedule successfully fenced for one sweep execution. */
|
|
528
|
+
export interface ClaimedDocumentPublishSchedule extends DocumentPublishSchedule {
|
|
529
|
+
executionToken: string;
|
|
530
|
+
executionExpiresAt: Date;
|
|
531
|
+
lastAttemptAt: Date;
|
|
532
|
+
/** Database time used for the claim, exposed for diagnostics and tests. */
|
|
533
|
+
databaseNow: Date;
|
|
534
|
+
/** True when this claim replaced an execution token whose lease had expired. */
|
|
535
|
+
recoveredExpiredClaim: boolean;
|
|
536
|
+
}
|
|
537
|
+
export type ScheduleDocumentPublishResult = {
|
|
538
|
+
status: 'scheduled';
|
|
539
|
+
schedule: DocumentPublishSchedule;
|
|
540
|
+
/** Null on first creation; the locked prior row on a reschedule. */
|
|
541
|
+
previous: DocumentPublishSchedule | null;
|
|
542
|
+
} | {
|
|
543
|
+
status: 'document_not_found' | 'version_mismatch' | 'publish_at_not_future' | 'execution_in_progress';
|
|
544
|
+
};
|
|
545
|
+
export type ConfirmDocumentPublishScheduleResult = {
|
|
546
|
+
status: 'confirmed';
|
|
547
|
+
schedule: DocumentPublishSchedule;
|
|
548
|
+
previousTargetVersionId: string;
|
|
549
|
+
} | {
|
|
550
|
+
status: 'schedule_not_found' | 'not_suspended' | 'version_mismatch';
|
|
551
|
+
};
|
|
552
|
+
export type SuspendDocumentPublishScheduleResult = {
|
|
553
|
+
status: 'suspended';
|
|
554
|
+
schedule: DocumentPublishSchedule;
|
|
555
|
+
} | {
|
|
556
|
+
status: 'schedule_not_found' | 'already_suspended';
|
|
557
|
+
};
|
|
558
|
+
export interface DocumentPublishSchedulePage {
|
|
559
|
+
schedules: DocumentPublishSchedule[];
|
|
560
|
+
total: number;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Transaction-aware scheduled-publication writes attached to the document
|
|
564
|
+
* command surface. They are storage primitives, not lifecycle or public SDK
|
|
565
|
+
* operations: core owns abilities, workflow validation, hooks, and audit.
|
|
566
|
+
*/
|
|
567
|
+
export interface IDocumentPublishScheduleCommands {
|
|
568
|
+
/**
|
|
569
|
+
* Create or reschedule one document's intent.
|
|
570
|
+
*
|
|
571
|
+
* The command locks the logical document and any existing schedule. It
|
|
572
|
+
* succeeds only when `expectedVersionId` is the current live version for
|
|
573
|
+
* `documentId`/`collectionId`, and when `publishAt` is strictly later than
|
|
574
|
+
* database time. A live execution claim returns `execution_in_progress`;
|
|
575
|
+
* an expired claim may be replaced by the reschedule.
|
|
576
|
+
*
|
|
577
|
+
* First creation stamps both actor columns. Rescheduling preserves
|
|
578
|
+
* `scheduledBy`, rewrites `lastAuthorizedBy`/`lastAuthorizedAt`, returns the
|
|
579
|
+
* row to `armed`, and clears all suspension, attempt, error, and execution
|
|
580
|
+
* state. `nextAttemptAt` becomes `publishAt`.
|
|
581
|
+
*
|
|
582
|
+
* Call inside `IDbAdapter.withTransaction` so the row and lifecycle audit
|
|
583
|
+
* append share the ambient transaction. The canonical adapters' tests pin
|
|
584
|
+
* that rollback behaviour.
|
|
585
|
+
*/
|
|
586
|
+
schedule(params: {
|
|
587
|
+
documentId: string;
|
|
588
|
+
collectionId: string;
|
|
589
|
+
expectedVersionId: string;
|
|
590
|
+
publishAt: Date;
|
|
591
|
+
actorId: string | null;
|
|
592
|
+
}): Promise<ScheduleDocumentPublishResult>;
|
|
593
|
+
/**
|
|
594
|
+
* Re-authorize a `needs_reconfirm` row against the current live version.
|
|
595
|
+
* Preserves `publishAt` and `scheduledBy`, including when `publishAt` is now
|
|
596
|
+
* in the past; rewrites the current authorization and clears suspension,
|
|
597
|
+
* attempt, error, and execution state. `nextAttemptAt` becomes `publishAt`.
|
|
598
|
+
* Must run inside the caller's ambient transaction.
|
|
599
|
+
*/
|
|
600
|
+
confirm(params: {
|
|
601
|
+
documentId: string;
|
|
602
|
+
collectionId: string;
|
|
603
|
+
expectedVersionId: string;
|
|
604
|
+
actorId: string | null;
|
|
605
|
+
}): Promise<ConfirmDocumentPublishScheduleResult>;
|
|
606
|
+
/**
|
|
607
|
+
* Lock and delete the document's row regardless of whether it currently
|
|
608
|
+
* carries an execution token. Cancellation and token-guarded publication
|
|
609
|
+
* therefore have one database-lock winner. Returns the deleted snapshot or
|
|
610
|
+
* null. Must run inside the caller's ambient transaction.
|
|
611
|
+
*/
|
|
612
|
+
cancel(params: {
|
|
613
|
+
documentId: string;
|
|
614
|
+
collectionId: string;
|
|
615
|
+
}): Promise<DocumentPublishSchedule | null>;
|
|
616
|
+
/**
|
|
617
|
+
* Move an armed row to `needs_reconfirm` after a content-version write,
|
|
618
|
+
* preserving its pinned target and authorization while clearing any claim.
|
|
619
|
+
* Repeated edits of an already-suspended row report `already_suspended`.
|
|
620
|
+
* Must run inside the version write's ambient transaction.
|
|
621
|
+
*/
|
|
622
|
+
suspendForContentEdit(params: {
|
|
623
|
+
documentId: string;
|
|
624
|
+
collectionId: string;
|
|
625
|
+
}): Promise<SuspendDocumentPublishScheduleResult>;
|
|
626
|
+
/**
|
|
627
|
+
* Atomically claim up to `batchSize` due rows, oldest `publishAt` first.
|
|
628
|
+
* Eligibility is derived only from database time: `armed`, both
|
|
629
|
+
* `publishAt` and `nextAttemptAt` due, and no live execution claim. Each
|
|
630
|
+
* winner receives its own fresh token; claim records database-now in
|
|
631
|
+
* `lastAttemptAt`, increments `attemptCount`, and sets the expiry.
|
|
632
|
+
* Concurrent callers skip locked rows rather than serializing the batch.
|
|
633
|
+
* This operation owns its short claim transaction and must not be wrapped
|
|
634
|
+
* in a lifecycle transaction.
|
|
635
|
+
*/
|
|
636
|
+
claimDue(params: {
|
|
637
|
+
batchSize: number;
|
|
638
|
+
leaseMs: number;
|
|
639
|
+
}): Promise<ClaimedDocumentPublishSchedule[]>;
|
|
640
|
+
/**
|
|
641
|
+
* Lock and return a token-matched row inside the publication transition's
|
|
642
|
+
* ambient transaction. Expiry alone does not invalidate a still-matching
|
|
643
|
+
* token: it becomes stale only when another claimant replaces it. A
|
|
644
|
+
* malformed or stale token returns null, never a driver error.
|
|
645
|
+
*/
|
|
646
|
+
lockClaim(params: {
|
|
647
|
+
documentId: string;
|
|
648
|
+
executionToken: string;
|
|
649
|
+
}): Promise<DocumentPublishSchedule | null>;
|
|
650
|
+
/**
|
|
651
|
+
* Delete a token-matched row inside the caller's ambient transaction.
|
|
652
|
+
* Returns false for malformed or stale tokens without mutating the row.
|
|
653
|
+
*/
|
|
654
|
+
deleteClaim(params: {
|
|
655
|
+
documentId: string;
|
|
656
|
+
executionToken: string;
|
|
657
|
+
}): Promise<boolean>;
|
|
658
|
+
/**
|
|
659
|
+
* Token-matched form of edit suspension for the fire-time version race.
|
|
660
|
+
* Clears the execution claim and attempt/error state. Returns false for a
|
|
661
|
+
* stale token without mutating the newer claimant's row.
|
|
662
|
+
*/
|
|
663
|
+
suspendClaimForContentEdit(params: {
|
|
664
|
+
documentId: string;
|
|
665
|
+
executionToken: string;
|
|
666
|
+
}): Promise<boolean>;
|
|
667
|
+
/**
|
|
668
|
+
* Release a failed token-matched attempt. Clears the execution claim,
|
|
669
|
+
* stores a sanitized error capped at 2 KiB, and sets `nextAttemptAt` from
|
|
670
|
+
* database time using the already-incremented `attemptCount`: 1, 2, 4, 8,
|
|
671
|
+
* then 15 minutes for the fifth and later attempts. Returns false for a
|
|
672
|
+
* malformed or stale token without overwriting a newer claimant.
|
|
673
|
+
*/
|
|
674
|
+
releaseClaim(params: {
|
|
675
|
+
documentId: string;
|
|
676
|
+
executionToken: string;
|
|
677
|
+
error: string;
|
|
678
|
+
}): Promise<boolean>;
|
|
679
|
+
}
|
|
680
|
+
/** Actor-agnostic scheduled-publication reads attached to document queries. */
|
|
681
|
+
export interface IDocumentPublishScheduleQueries {
|
|
682
|
+
/** Return one collection-scoped schedule, or null. */
|
|
683
|
+
get(params: {
|
|
684
|
+
documentId: string;
|
|
685
|
+
collectionId: string;
|
|
686
|
+
}): Promise<DocumentPublishSchedule | null>;
|
|
687
|
+
/**
|
|
688
|
+
* Return a deterministic page ordered by `publishAt`, then `documentId`.
|
|
689
|
+
* The adapter must enforce `collectionIds` with `IN (...)`; an empty
|
|
690
|
+
* allowlist returns `{ schedules: [], total: 0 }` without issuing SQL.
|
|
691
|
+
* Optional state/authorizer filters are storage predicates only—ability
|
|
692
|
+
* resolution remains in core.
|
|
693
|
+
*/
|
|
694
|
+
list(params: {
|
|
695
|
+
collectionIds: readonly string[];
|
|
696
|
+
states?: readonly DocumentPublishScheduleState[];
|
|
697
|
+
lastAuthorizedBy?: string;
|
|
698
|
+
page: number;
|
|
699
|
+
pageSize: number;
|
|
700
|
+
}): Promise<DocumentPublishSchedulePage>;
|
|
701
|
+
}
|
|
485
702
|
export interface IDocumentCommands {
|
|
703
|
+
/** Scheduled-publication storage primitives; lifecycle rules remain in core. */
|
|
704
|
+
publishSchedules: IDocumentPublishScheduleCommands;
|
|
486
705
|
createDocumentVersion(params: {
|
|
487
706
|
documentId?: string;
|
|
488
707
|
collectionId: string;
|
|
@@ -708,6 +927,8 @@ export interface ICollectionQueries {
|
|
|
708
927
|
getCollectionById(id: string): Promise<any>;
|
|
709
928
|
}
|
|
710
929
|
export interface IDocumentQueries {
|
|
930
|
+
/** Scheduled-publication reads; callers resolve collection abilities above storage. */
|
|
931
|
+
publishSchedules: IDocumentPublishScheduleQueries;
|
|
711
932
|
/**
|
|
712
933
|
* Lock a logical document as the transaction mutex for its non-versioned
|
|
713
934
|
* system fields, then return their authoritative current values. Must be
|
|
@@ -1124,3 +1345,4 @@ export interface IDocumentQueries {
|
|
|
1124
1345
|
order_key: string;
|
|
1125
1346
|
}>>;
|
|
1126
1347
|
}
|
|
1348
|
+
export {};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { SessionProvider } from '@byline/auth';
|
|
9
|
+
import type { RecurringTaskDefinition } from '../scheduler/types.js';
|
|
9
10
|
import type { SlugifierFn } from '../utils/slugify.js';
|
|
10
11
|
import type { FilenameSlugifierFn } from '../utils/slugify-filename.js';
|
|
11
12
|
import type { BlockAdminConfig, CollectionAdminConfig, CollectionGroupDefinition } from './admin-types.js';
|
|
@@ -241,6 +242,15 @@ export interface ServerHooksConfig {
|
|
|
241
242
|
collections?: Record<string, CollectionHooks | CollectionHooksLoader>;
|
|
242
243
|
uploads?: Record<string, UploadHooks | UploadHooksLoader>;
|
|
243
244
|
}
|
|
245
|
+
/** Installation-wide switch for Byline's scheduled-publication subsystem. */
|
|
246
|
+
export interface ScheduledPublicationConfig {
|
|
247
|
+
/**
|
|
248
|
+
* Register the built-in `documents.publish-scheduled` recurring task and
|
|
249
|
+
* signal host/admin integrations to expose the feature. Registration
|
|
250
|
+
* remains inert until the host explicitly starts the scheduler.
|
|
251
|
+
*/
|
|
252
|
+
enabled: boolean;
|
|
253
|
+
}
|
|
244
254
|
/**
|
|
245
255
|
* Server-side configuration. Extends BaseConfig with database and storage
|
|
246
256
|
* adapters. Deliberately does NOT extend AdminConfig — the server has no
|
|
@@ -444,6 +454,20 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
444
454
|
* ```
|
|
445
455
|
*/
|
|
446
456
|
search?: SearchProvider;
|
|
457
|
+
/**
|
|
458
|
+
* Optional delayed-publication subsystem. Enabling it contributes the
|
|
459
|
+
* built-in recurring task to `BylineCore.recurringTasks`; it never starts a
|
|
460
|
+
* timer during `initBylineCore()`.
|
|
461
|
+
*/
|
|
462
|
+
scheduledPublication?: ScheduledPublicationConfig;
|
|
463
|
+
/**
|
|
464
|
+
* Recurring background tasks (`defineRecurringTask()`), validated and gated
|
|
465
|
+
* against the configured adapter's optional scheduler capability by
|
|
466
|
+
* `initBylineCore()`. Registration is not execution — the validated set is
|
|
467
|
+
* exposed as `BylineCore.recurringTasks` for `runDueTasks()` /
|
|
468
|
+
* `startBylineScheduler()` to consume; nothing here starts a timer.
|
|
469
|
+
*/
|
|
470
|
+
recurringTasks?: readonly RecurringTaskDefinition[];
|
|
447
471
|
}
|
|
448
472
|
/** Server config returned after boundary validation and canonicalization. */
|
|
449
473
|
export type ResolvedServerConfig<TAdminStore = unknown> = Omit<ServerConfig<TAdminStore>, 'routes'> & {
|
|
@@ -243,6 +243,6 @@ describe('emitCollectionTypes', () => {
|
|
|
243
243
|
import: './dist/codegen/index.js',
|
|
244
244
|
require: './dist/codegen/index.js',
|
|
245
245
|
});
|
|
246
|
-
expect(rootSource).not.toMatch(/codegen/);
|
|
246
|
+
expect(rootSource).not.toMatch(/['"]\.\/codegen(?:\/[^'"]*)?['"]/u);
|
|
247
247
|
});
|
|
248
248
|
});
|
package/dist/core.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Logger as PinoLogger } from 'pino';
|
|
|
10
10
|
import { type BylineLogger } from './lib/logger.js';
|
|
11
11
|
import { type CollectionRecord } from './services/collection-bootstrap.js';
|
|
12
12
|
import type { CollectionDefinition, IDbAdapter, IStorageProvider, ResolvedServerConfig, ServerConfig } from './@types/index.js';
|
|
13
|
+
import type { RecurringTaskDefinition } from './scheduler/types.js';
|
|
13
14
|
export interface BylineCore<TAdminStore = unknown> {
|
|
14
15
|
config: ResolvedServerConfig<TAdminStore>;
|
|
15
16
|
collections: readonly CollectionDefinition[];
|
|
@@ -63,6 +64,13 @@ export interface BylineCore<TAdminStore = unknown> {
|
|
|
63
64
|
* Undefined when the installation does not configure admin.
|
|
64
65
|
*/
|
|
65
66
|
adminStore: TAdminStore | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Validated recurring-task definitions (`ServerConfig.recurringTasks`),
|
|
69
|
+
* empty when none are configured. `runDueTasks(core)` and
|
|
70
|
+
* `startBylineScheduler(core)` read this vetted set so no caller can
|
|
71
|
+
* substitute another; `initBylineCore()` does not start a timer.
|
|
72
|
+
*/
|
|
73
|
+
recurringTasks: readonly RecurringTaskDefinition[];
|
|
66
74
|
}
|
|
67
75
|
/**
|
|
68
76
|
* Initialize Byline CMS core services via the typed registry.
|
package/dist/core.js
CHANGED
|
@@ -10,6 +10,8 @@ import { registerCollectionAbilities } from './auth/register-collection-abilitie
|
|
|
10
10
|
import { defineBylineCore, getBylineCoreUnsafe, registerServerConfig, resolveServerConfig, } from './config/config.js';
|
|
11
11
|
import { createBylineLogger, defineLogger } from './lib/logger.js';
|
|
12
12
|
import { Registry } from './lib/registry.js';
|
|
13
|
+
import { SCHEDULED_PUBLICATION_INTERVAL_MS, SCHEDULED_PUBLICATION_LEASE_MS, SCHEDULED_PUBLICATION_TASK_NAME, } from './scheduler/scheduled-publication-constants.js';
|
|
14
|
+
import { validateSchedulerConfig } from './scheduler/validate-scheduler-config.js';
|
|
13
15
|
import { ensureCollections } from './services/collection-bootstrap.js';
|
|
14
16
|
import { discoverCounterGroups } from './services/discover-counter-groups.js';
|
|
15
17
|
import { validateTreeAuditCapability } from './services/document-lifecycle/audit.js';
|
|
@@ -41,6 +43,7 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
41
43
|
.addValue('storage', resolvedConfig.storage)
|
|
42
44
|
.addFactory('logger', createBylineLogger);
|
|
43
45
|
const composed = registry.compose({ pinoLogger });
|
|
46
|
+
let initializedCore;
|
|
44
47
|
// Validate richText field flags against the registered server adapter
|
|
45
48
|
// before any DB work. Fail-fast surfaces unrenderable configurations
|
|
46
49
|
// (both flags off) and missing-adapter cases at boot rather than at
|
|
@@ -55,6 +58,40 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
55
58
|
validateSearchConfig(composed.collections, {
|
|
56
59
|
provider: resolvedConfig.search != null,
|
|
57
60
|
});
|
|
61
|
+
// Validate recurring-task configuration: a task registered against an
|
|
62
|
+
// adapter that does not implement the optional scheduler capability would
|
|
63
|
+
// silently never run. Fail-fast at boot, same posture as search above. This
|
|
64
|
+
// only validates and gates — it does not start anything.
|
|
65
|
+
const configuredTasks = [...(resolvedConfig.recurringTasks ?? [])];
|
|
66
|
+
if (resolvedConfig.scheduledPublication?.enabled === true) {
|
|
67
|
+
configuredTasks.push({
|
|
68
|
+
name: SCHEDULED_PUBLICATION_TASK_NAME,
|
|
69
|
+
intervalMs: SCHEDULED_PUBLICATION_INTERVAL_MS,
|
|
70
|
+
leaseMs: SCHEDULED_PUBLICATION_LEASE_MS,
|
|
71
|
+
run: async (context) => {
|
|
72
|
+
if (initializedCore === undefined) {
|
|
73
|
+
throw new Error('scheduled publication task ran before Byline core initialization');
|
|
74
|
+
}
|
|
75
|
+
const { runScheduledPublicationSweep } = await import('./scheduler/scheduled-publication.js');
|
|
76
|
+
const result = await runScheduledPublicationSweep(initializedCore, {
|
|
77
|
+
signal: context.signal,
|
|
78
|
+
heartbeat: context.heartbeat,
|
|
79
|
+
logger: context.logger,
|
|
80
|
+
});
|
|
81
|
+
return { workRemaining: result.workRemaining };
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
validateSchedulerConfig({ tasks: configuredTasks, adapter: composed.db });
|
|
86
|
+
// Freeze a snapshot of the validated task set: a new array of new, frozen
|
|
87
|
+
// objects, then freeze the array itself. Without this, `core.recurringTasks`
|
|
88
|
+
// would hold the caller's own array and definition objects, so a caller
|
|
89
|
+
// could `push()` a task or mutate `intervalMs` after `initBylineCore()`
|
|
90
|
+
// returns and bypass validation entirely. Both `core.recurringTasks` and
|
|
91
|
+
// the resolved config's `recurringTasks` are assigned this same snapshot so
|
|
92
|
+
// no path exposes the caller's originals.
|
|
93
|
+
const recurringTasks = Object.freeze(configuredTasks.map((task) => Object.freeze({ ...task })));
|
|
94
|
+
resolvedConfig.recurringTasks = recurringTasks;
|
|
58
95
|
// Tree edges are unversioned metadata and may only run on adapters that can
|
|
59
96
|
// lock, mutate, and append audit rows in one transaction.
|
|
60
97
|
validateTreeAuditCapability(composed.collections, composed.db);
|
|
@@ -141,7 +178,9 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
141
178
|
getAbilitiesByGroup: () => abilities.byGroup(),
|
|
142
179
|
sessionProvider: composed.config.sessionProvider,
|
|
143
180
|
adminStore: composed.config.adminStore,
|
|
181
|
+
recurringTasks,
|
|
144
182
|
};
|
|
183
|
+
initializedCore = core;
|
|
145
184
|
// Commit globals only after the replacement core has fully initialized. A
|
|
146
185
|
// failed reinitialization leaves the prior config, logger, and core intact.
|
|
147
186
|
registerServerConfig(resolvedConfig);
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { AsyncRegistry, type RegisteredServices, Registry } from './lib/registry
|
|
|
15
15
|
export * from './patches/index.js';
|
|
16
16
|
export * from './paths/index.js';
|
|
17
17
|
export { mergePredicates, type ParseContext, type ParsedSort, type ParsedWhere, parsePredicateFilters, parseSort, parseWhere, } from './query/parse-where.js';
|
|
18
|
+
export { defineRecurringTask, MIN_INTERVAL_MS, MIN_LEASE_MS, } from './scheduler/define-recurring-task.js';
|
|
18
19
|
export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
|
|
19
20
|
export * from './services/index.js';
|
|
20
21
|
export * from './storage/index.js';
|
|
@@ -23,3 +24,4 @@ export { formatTextValue, looksLikeISODate, type SlugifierFn, type SlugifyContex
|
|
|
23
24
|
export { type FilenameSlugifierFn, type FilenameSlugifyContext, resolveUploadFilename, slugifyFilename, } from './utils/slugify-filename.js';
|
|
24
25
|
export { getUploadFields, hasUploadField, isUploadField } from './utils/storage-utils.js';
|
|
25
26
|
export * from './workflow/index.js';
|
|
27
|
+
export type { ClaimedRecurringTask, ISchedulerStore, ReconcileTaskInput, RecurringTaskContext, RecurringTaskDefinition, RecurringTaskHealth, RecurringTaskResult, RecurringTaskStatus, } from './scheduler/types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// @byline/core public surface.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// - `./patches`, `./workflow`, `./services` — NOT published
|
|
4
|
+
// The main entry (this file) is browser-safe. Capability-specific surfaces use
|
|
5
|
+
// the explicit subpaths declared in `package.json`, such as `./codegen` and
|
|
6
|
+
// the server-only `./scheduler` entry.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// `publishConfig.exports` — they are not stable surface and should
|
|
14
|
-
// not be imported by external npm consumers. External access goes
|
|
15
|
-
// through this main entry or `@byline/client`.
|
|
8
|
+
// Every key in the package's `exports` map is included in the published npm
|
|
9
|
+
// package and may be imported by external consumers. A subpath's primary use
|
|
10
|
+
// inside this monorepo does not make it private; removing or renaming one is a
|
|
11
|
+
// breaking package-surface change.
|
|
16
12
|
// ---------------------------------------------------------------------------
|
|
17
13
|
export * from './@types/index.js';
|
|
18
14
|
export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, collectionAbilityKey, compileBeforeReadFilters, filterReadableCollections, registerCollectionAbilities, } from './auth/index.js';
|
|
@@ -31,6 +27,7 @@ export { AsyncRegistry, Registry } from './lib/registry.js';
|
|
|
31
27
|
export * from './patches/index.js';
|
|
32
28
|
export * from './paths/index.js';
|
|
33
29
|
export { mergePredicates, parsePredicateFilters, parseSort, parseWhere, } from './query/parse-where.js';
|
|
30
|
+
export { defineRecurringTask, MIN_INTERVAL_MS, MIN_LEASE_MS, } from './scheduler/define-recurring-task.js';
|
|
34
31
|
export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
|
|
35
32
|
export * from './services/index.js';
|
|
36
33
|
export * from './storage/index.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
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 { RecurringTaskDefinition } from './types.js';
|
|
9
|
+
/** Shortest permitted interval between runs, and shortest permitted lease. */
|
|
10
|
+
export declare const MIN_INTERVAL_MS = 60000;
|
|
11
|
+
export declare const MIN_LEASE_MS = 60000;
|
|
12
|
+
/** Maximum bounded retry delay after repeated failures. */
|
|
13
|
+
export declare const MAX_BACKOFF_MS: number;
|
|
14
|
+
/**
|
|
15
|
+
* Identity helper that gives a task definition its type without starting
|
|
16
|
+
* anything. Registration is not execution: timers begin only when the host
|
|
17
|
+
* calls `startBylineScheduler()`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function defineRecurringTask(definition: RecurringTaskDefinition): RecurringTaskDefinition;
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
/** Shortest permitted interval between runs, and shortest permitted lease. */
|
|
9
|
+
export const MIN_INTERVAL_MS = 60_000;
|
|
10
|
+
export const MIN_LEASE_MS = 60_000;
|
|
11
|
+
/** Maximum bounded retry delay after repeated failures. */
|
|
12
|
+
export const MAX_BACKOFF_MS = 15 * 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* Identity helper that gives a task definition its type without starting
|
|
15
|
+
* anything. Registration is not execution: timers begin only when the host
|
|
16
|
+
* calls `startBylineScheduler()`.
|
|
17
|
+
*/
|
|
18
|
+
export function defineRecurringTask(definition) {
|
|
19
|
+
return definition;
|
|
20
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
* `@byline/core/scheduler` — the server-only executable surface of the
|
|
10
|
+
* recurring-task scheduler. The inert types and `defineRecurringTask()` are
|
|
11
|
+
* also re-exported from the package root; the runner and ticker are here so
|
|
12
|
+
* importing browser-safe core code never pulls in Node timers.
|
|
13
|
+
*/
|
|
14
|
+
export { defineRecurringTask, MAX_BACKOFF_MS, MIN_INTERVAL_MS, MIN_LEASE_MS, } from './define-recurring-task.js';
|
|
15
|
+
export { type RunDueTasksOptions, type RunDueTasksSummary, runDueTasks } from './run-due-tasks.js';
|
|
16
|
+
export { runScheduledPublicationSweep, type ScheduledPublicationSweepOptions, type ScheduledPublicationSweepResult, } from './scheduled-publication.js';
|
|
17
|
+
export { type SchedulerController, type SchedulerOptions, startBylineScheduler, } from './ticker.js';
|
|
18
|
+
export { validateRecurringTasks } from './validate-tasks.js';
|
|
19
|
+
export type { ClaimedRecurringTask, ISchedulerStore, ReconcileTaskInput, RecurringTaskContext, RecurringTaskDefinition, RecurringTaskHealth, RecurringTaskResult, RecurringTaskStatus, } from './types.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
* `@byline/core/scheduler` — the server-only executable surface of the
|
|
10
|
+
* recurring-task scheduler. The inert types and `defineRecurringTask()` are
|
|
11
|
+
* also re-exported from the package root; the runner and ticker are here so
|
|
12
|
+
* importing browser-safe core code never pulls in Node timers.
|
|
13
|
+
*/
|
|
14
|
+
export { defineRecurringTask, MAX_BACKOFF_MS, MIN_INTERVAL_MS, MIN_LEASE_MS, } from './define-recurring-task.js';
|
|
15
|
+
export { runDueTasks } from './run-due-tasks.js';
|
|
16
|
+
export { runScheduledPublicationSweep, } from './scheduled-publication.js';
|
|
17
|
+
export { startBylineScheduler, } from './ticker.js';
|
|
18
|
+
export { validateRecurringTasks } from './validate-tasks.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { BylineCore } from '../core.js';
|
|
9
|
+
import type { BylineLogger } from '../logger/index.js';
|
|
10
|
+
import type { ISchedulerStore, RecurringTaskDefinition } from './types.js';
|
|
11
|
+
export interface RunDueTasksOptions {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
concurrency?: number;
|
|
14
|
+
owner?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface RunDueTasksSummary {
|
|
17
|
+
claimed: number;
|
|
18
|
+
succeeded: number;
|
|
19
|
+
failed: number;
|
|
20
|
+
aborted: number;
|
|
21
|
+
}
|
|
22
|
+
interface RunDueTasksDeps {
|
|
23
|
+
store: ISchedulerStore;
|
|
24
|
+
tasks: readonly RecurringTaskDefinition[];
|
|
25
|
+
owner: string;
|
|
26
|
+
logger: BylineLogger;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
concurrency?: number;
|
|
29
|
+
}
|
|
30
|
+
/** A bounded, non-secret diagnostic label. Correctness never depends on it. */
|
|
31
|
+
export declare function defaultOwner(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Dependency-injected implementation used by the ticker and unit tests. It is
|
|
34
|
+
* intentionally absent from the `@byline/core/scheduler` barrel.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runDueTasksWithDeps(params: RunDueTasksDeps): Promise<RunDueTasksSummary>;
|
|
37
|
+
/** Run one claim-and-run pass over the task definitions vetted at core boot. */
|
|
38
|
+
export declare function runDueTasks(core: BylineCore, options?: RunDueTasksOptions): Promise<RunDueTasksSummary>;
|
|
39
|
+
export {};
|