@byline/core 4.14.0 → 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.
Files changed (62) hide show
  1. package/dist/@types/db-types.d.ts +222 -0
  2. package/dist/@types/site-config.d.ts +24 -0
  3. package/dist/codegen/index.test.node.js +1 -1
  4. package/dist/core.d.ts +8 -0
  5. package/dist/core.js +39 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +8 -11
  8. package/dist/scheduler/define-recurring-task.d.ts +19 -0
  9. package/dist/scheduler/define-recurring-task.js +20 -0
  10. package/dist/scheduler/index.d.ts +19 -0
  11. package/dist/scheduler/index.js +18 -0
  12. package/dist/scheduler/run-due-tasks.d.ts +39 -0
  13. package/dist/scheduler/run-due-tasks.js +324 -0
  14. package/dist/scheduler/run-due-tasks.test.node.d.ts +8 -0
  15. package/dist/scheduler/run-due-tasks.test.node.js +396 -0
  16. package/dist/scheduler/scheduled-publication-constants.d.ts +11 -0
  17. package/dist/scheduler/scheduled-publication-constants.js +11 -0
  18. package/dist/scheduler/scheduled-publication.d.ts +31 -0
  19. package/dist/scheduler/scheduled-publication.js +198 -0
  20. package/dist/scheduler/scheduled-publication.test.node.d.ts +8 -0
  21. package/dist/scheduler/scheduled-publication.test.node.js +109 -0
  22. package/dist/scheduler/scheduler-boot.test.node.d.ts +8 -0
  23. package/dist/scheduler/scheduler-boot.test.node.js +103 -0
  24. package/dist/scheduler/ticker.d.ts +34 -0
  25. package/dist/scheduler/ticker.js +144 -0
  26. package/dist/scheduler/ticker.test.node.d.ts +8 -0
  27. package/dist/scheduler/ticker.test.node.js +249 -0
  28. package/dist/scheduler/types.d.ts +241 -0
  29. package/dist/scheduler/types.js +8 -0
  30. package/dist/scheduler/validate-scheduler-config.d.ts +19 -0
  31. package/dist/scheduler/validate-scheduler-config.js +25 -0
  32. package/dist/scheduler/validate-scheduler-config.test.node.d.ts +8 -0
  33. package/dist/scheduler/validate-scheduler-config.test.node.js +38 -0
  34. package/dist/scheduler/validate-tasks.d.ts +14 -0
  35. package/dist/scheduler/validate-tasks.js +40 -0
  36. package/dist/scheduler/validate-tasks.test.node.d.ts +8 -0
  37. package/dist/scheduler/validate-tasks.test.node.js +49 -0
  38. package/dist/services/collection-bootstrap.test.node.js +2 -0
  39. package/dist/services/discover-counter-groups.test.node.js +2 -0
  40. package/dist/services/document-lifecycle/audit.d.ts +6 -0
  41. package/dist/services/document-lifecycle/audit.js +6 -0
  42. package/dist/services/document-lifecycle/copy-to-locale.js +15 -10
  43. package/dist/services/document-lifecycle/delete-locale.js +18 -10
  44. package/dist/services/document-lifecycle/delete.js +8 -0
  45. package/dist/services/document-lifecycle/index.d.ts +1 -0
  46. package/dist/services/document-lifecycle/index.js +1 -0
  47. package/dist/services/document-lifecycle/publish-schedule-consistency.d.ts +36 -0
  48. package/dist/services/document-lifecycle/publish-schedule-consistency.js +80 -0
  49. package/dist/services/document-lifecycle/restore.js +15 -10
  50. package/dist/services/document-lifecycle/scheduled-publish.d.ts +51 -0
  51. package/dist/services/document-lifecycle/scheduled-publish.js +351 -0
  52. package/dist/services/document-lifecycle/status-transition.d.ts +42 -0
  53. package/dist/services/document-lifecycle/status-transition.js +41 -0
  54. package/dist/services/document-lifecycle/status-transition.test.node.d.ts +8 -0
  55. package/dist/services/document-lifecycle/status-transition.test.node.js +145 -0
  56. package/dist/services/document-lifecycle/status.js +30 -23
  57. package/dist/services/document-lifecycle/tree.test.node.js +3 -0
  58. package/dist/services/document-lifecycle/update.js +35 -30
  59. package/dist/services/document-lifecycle.test.node.js +262 -2
  60. package/dist/services/field-upload.test.node.js +2 -0
  61. package/dist/services/populate.test.node.js +2 -0
  62. 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
- // Subpath exports (see `package.json`):
5
- // - `.` — main entry (this file); published
6
- // - `./zod-schemas`, `./logger`, `./package.json` — published
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
- // The three unpublished subpaths are in-monorepo boundaries used by
10
- // the admin server fns and the `@byline/client` SDK. They are
11
- // registered in the main `exports` map (so workspace consumers and
12
- // `tsc` can resolve them) but deliberately omitted from
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 {};