@forumone/throughline-workflows 0.3.1 → 0.5.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 (38) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +176 -10
  3. package/dist/cache-tags.d.ts +31 -0
  4. package/dist/cache-tags.d.ts.map +1 -0
  5. package/dist/cache-tags.js +53 -0
  6. package/dist/cache-tags.js.map +1 -0
  7. package/dist/execute-scheduled-publishes.d.ts +5 -1
  8. package/dist/execute-scheduled-publishes.d.ts.map +1 -1
  9. package/dist/execute-scheduled-publishes.js +17 -3
  10. package/dist/execute-scheduled-publishes.js.map +1 -1
  11. package/dist/failure-handler.d.ts +38 -0
  12. package/dist/failure-handler.d.ts.map +1 -0
  13. package/dist/failure-handler.js +60 -0
  14. package/dist/failure-handler.js.map +1 -0
  15. package/dist/index.d.ts +8 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +8 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/next-revalidate.d.ts +43 -0
  20. package/dist/next-revalidate.d.ts.map +1 -0
  21. package/dist/next-revalidate.js +42 -0
  22. package/dist/next-revalidate.js.map +1 -0
  23. package/dist/publish-at-scheduled-time.d.ts +23 -0
  24. package/dist/publish-at-scheduled-time.d.ts.map +1 -0
  25. package/dist/publish-at-scheduled-time.js +133 -0
  26. package/dist/publish-at-scheduled-time.js.map +1 -0
  27. package/dist/revalidate-on-publish.d.ts +10 -0
  28. package/dist/revalidate-on-publish.d.ts.map +1 -1
  29. package/dist/revalidate-on-publish.js +21 -25
  30. package/dist/revalidate-on-publish.js.map +1 -1
  31. package/dist/revalidate-tag-hooks.d.ts +80 -0
  32. package/dist/revalidate-tag-hooks.d.ts.map +1 -0
  33. package/dist/revalidate-tag-hooks.js +116 -0
  34. package/dist/revalidate-tag-hooks.js.map +1 -0
  35. package/dist/types.d.ts +50 -7
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/types.js.map +1 -1
  38. package/package.json +10 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # @forumone/throughline-workflows
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ab623e1: The `payload` peer range moves from `^3.0.0` to `^3.89.0` for every package that has one. **A site on Payload older than 3.89.0 must upgrade Payload before upgrading these packages.**
8
+
9
+ Before 3.89.0, the `payload-mcp-api-keys` collection that `@payloadcms/plugin-mcp` adds registered Payload's API-key strategy on every REST route. Any key could then become `req.user` outside `/api/mcp` and pass access rules written as `Boolean(req.user)`. Every Throughline site runs that plugin, so the floor is the same for every package. No package's code changes with this bump.
10
+
11
+ - 653817e: Revalidation no longer guesses paths, and covers the changes a publish event never announces.
12
+
13
+ - **workflows (breaking)**: `createRevalidateOnPublishFunction` has no built-in URL
14
+ builders. They mapped `pages` to `/<slug>`, `posts` to `/blog/<slug>` and any other
15
+ collection to `/<slug>`, so a site whose routes differed revalidated the wrong path
16
+ without a word. `urlBuilders` is now required; a collection with no entry has its
17
+ tags dropped and no path revalidated, and the run logs a warning. To keep the old
18
+ behaviour, pass the old builders:
19
+
20
+ ```ts
21
+ urlBuilders: {
22
+ pages: (slug) => (slug === 'home' || slug === '' ? '/' : `/${slug}`),
23
+ posts: (slug) => `/blog/${slug}`,
24
+ }
25
+ ```
26
+
27
+ and then check them against your routes.
28
+
29
+ - **workflows**: `createTagRevalidationHooks` — collection `afterChange` and
30
+ `afterDelete`, and global `afterChange`, hooks that call
31
+ `revalidateTag(tag, { expire: 0 })`. Draft saves and autosave drop nothing (via
32
+ publishing's `isDraftWrite`); an unpublish still does. A hook never throws: outside a
33
+ Next request (seeds, migrations, the CLI) it logs at `debug`, and any other failure
34
+ at `error` with the tag and the cause.
35
+ - **workflows**: `createCacheTags` builds every tag string, so the hooks, the publish
36
+ workflow (new `cacheTags` option) and cached reads cannot name different tags.
37
+ Defaults: the bare slug for a collection, `global_<slug>` for a global. Also on the
38
+ dependency-free `@forumone/throughline-workflows/cache-tags` subpath for frontend code.
39
+ `@forumone/throughline-publishing` is now a dependency of this package.
40
+ - **create-throughline**: the scaffold adds `apps/web/src/lib/cache-tags.ts`, attaches
41
+ the tag hooks to `Pages`, and passes explicit `urlBuilders` and the shared
42
+ `cacheTags` to the publish workflow.
43
+
44
+ - 70385c4: `onTerminalFailure` has a handler to pass it. `createTerminalFailureHandler({ payload })` logs a run that exhausted its retries, writes a `job-failures` row through core's `jobFailuresPlugin` when it is registered, and posts the report to `ERROR_WEBHOOK_URL`. `createHealthcheckFailureHandler({ payload })` does the same for the healthcheck's per-run `onFailure`. Neither throws, because a throwing `onFailure` is retried by Inngest. The terminal handler also works as the `onFailure` of any Inngest function.
45
+
46
+ Recording rows needs core's `jobFailuresPlugin` and its migration. Without the plugin, the handlers still log and report.
47
+
48
+ ### Patch Changes
49
+
50
+ - Updated dependencies [006ae30]
51
+ - Updated dependencies [549d292]
52
+ - Updated dependencies [70385c4]
53
+ - Updated dependencies [c8a86bf]
54
+ - Updated dependencies [ab623e1]
55
+ - Updated dependencies [ab623e1]
56
+ - Updated dependencies [36728c4]
57
+ - @forumone/throughline-core@0.10.0
58
+ - @forumone/throughline-publishing@0.11.0
59
+
60
+ ## 0.4.0
61
+
62
+ ### Minor Changes
63
+
64
+ - 35060e5: Scheduled publishing works from the admin, publishes on time, and cleans up after itself.
65
+
66
+ A schedule set from the admin never published. It was written by a draft save,
67
+ Payload keeps a draft save in the versions table only, and the cron read the main
68
+ row — so only a schedule written over MCP, by a non-draft update, was ever found.
69
+ - **publishing**: a Schedule control replaces the scheduled-publish date field on
70
+ any collection that declares one, and runs the pipeline's checks when a time is
71
+ picked. An approval not yet granted, or an embargo that ends first, schedules
72
+ with a warning rather than refusing. `schedule` and `unschedule` admin
73
+ endpoints, `PublishingService.schedule` / `.unschedule`, and
74
+ `scheduleDocument` / `unscheduleDocument`; `schedule_publish` now calls the
75
+ service and writes the time as a draft. An `afterChange` hook sends
76
+ `content/page.scheduled` whenever the time changes, whichever route set it.
77
+ Publishing and unpublishing clear the time, so an unpublished page can no longer
78
+ put itself back up.
79
+ - **workflows**: `createPublishAtScheduledTimeFunction` sleeps until the scheduled
80
+ time instead of polling, in six-day hops for schedules further out.
81
+ `createExecuteScheduledPublishesFunction` reads the latest version
82
+ (`draft: true`) and takes `overdueByMs`, for running it as a backstop.
83
+
3
84
  ## 0.3.1
4
85
 
5
86
  ### Patch Changes
package/README.md CHANGED
@@ -1,18 +1,31 @@
1
1
  # @forumone/throughline-workflows
2
2
 
3
- Composable Inngest function factories for the Throughline framework. Client apps import the factories they need and merge the functions into their Inngest endpoint.
3
+ Composable Inngest function factories for the Throughline framework, and the Payload hooks that keep a Next cache honest. Client apps import the factories they need and merge the functions into their Inngest endpoint.
4
4
 
5
- This package has **no Payload plugin** — it exports factories only. Workflows subscribe to events the server packages emit; they don't modify Payload configuration.
5
+ This package has **no Payload plugin** — it exports factories only. Workflows subscribe to events the server packages emit; they don't modify Payload configuration. The cache-revalidation hooks are factories too: you attach them to the collections and globals you choose.
6
6
 
7
7
  ## What this package provides
8
8
 
9
9
  | Factory | What it does |
10
10
  |---|---|
11
11
  | `createRevalidateOnPublishFunction` | Invalidates Next.js caches for the page, listings, and sitemap when content publishes |
12
- | `createExecuteScheduledPublishesFunction` | Cron-driven scheduled publish executor that calls through the Publishing Server's MCP |
12
+ | `createPublishAtScheduledTimeFunction` | Publishes a document at its scheduled time by sleeping until then, woken by `content/page.scheduled` |
13
+ | `createExecuteScheduledPublishesFunction` | Cron that publishes any document past its scheduled time — the backstop for the one above, or a poller on its own |
13
14
  | `createExpireStaleApprovalsFunction` | Daily cron that flips pending approvals past `expiresAt` to `expired` and fires an `approval/expired` event |
14
15
  | `createAuditEventEchoFunction` | Fan-out point: turns `audit/event.recorded` into `notification/send-approval-*` events plus custom handlers |
15
16
  | `createHealthcheckFunction` | Periodic health monitoring with configurable checks |
17
+ | `createTagRevalidationHooks` | Payload `afterChange` / `afterDelete` hooks that drop Next cache tags when a collection or global changes |
18
+ | `createCacheTags` | The one tag scheme hooks, workflow and readers all build tags from (also on `@forumone/throughline-workflows/cache-tags`) |
19
+
20
+ Plus two failure handlers, for the `onTerminalFailure` every factory accepts and the healthcheck's own `onFailure`:
21
+
22
+ - `createTerminalFailureHandler({ payload })` — logs a run that exhausted its retries, writes a `job-failures` row (when core's `jobFailuresPlugin` is registered) and posts it to `ERROR_WEBHOOK_URL`. Never throws. Works as any Inngest function's `onFailure`.
23
+ - `createHealthcheckFailureHandler({ payload })` — the same, once per healthcheck run with failing checks.
24
+
25
+ ```ts
26
+ const onTerminalFailure = createTerminalFailureHandler({ payload })
27
+ createExpireStaleApprovalsFunction({ inngest, payload, onTerminalFailure })
28
+ ```
16
29
 
17
30
  Plus two reusable check helpers used with the healthcheck factory:
18
31
 
@@ -25,7 +38,7 @@ Plus two reusable check helpers used with the healthcheck factory:
25
38
  pnpm add @forumone/throughline-workflows
26
39
  ```
27
40
 
28
- Peers: `payload@^3.0.0`, `inngest@^4.0.0`. `next` is an **optional** peer — install it only if you use `createRevalidateOnPublishFunction` with the default revalidator.
41
+ Peers: `payload@^3.89.0`, `inngest@^4.0.0`. `next` is an **optional** peer — install it only if you use `createRevalidateOnPublishFunction` or `createTagRevalidationHooks` with their default revalidators.
29
42
 
30
43
  ## Usage
31
44
 
@@ -51,7 +64,16 @@ const payload = await getPayload({ config })
51
64
  export const { GET, POST, PUT } = serve({
52
65
  client: inngest,
53
66
  functions: [
54
- createRevalidateOnPublishFunction({ inngest, payload }),
67
+ createRevalidateOnPublishFunction({
68
+ inngest,
69
+ payload,
70
+ // Required. Where each publishable collection is served.
71
+ urlBuilders: {
72
+ pages: (slug) => (slug === 'home' ? '/' : `/${slug}`),
73
+ posts: (slug) => `/news/${slug}`,
74
+ },
75
+ cacheTags, // the same scheme your readers use — see below
76
+ }),
55
77
  createExecuteScheduledPublishesFunction({
56
78
  inngest,
57
79
  payload,
@@ -76,14 +98,116 @@ By C10, every server package fires Inngest events when consequential things happ
76
98
 
77
99
  The factories-only shape (no Payload plugin) is the same reasoning. A workflow that wants to revalidate Next.js pages doesn't need to register a Payload collection. A scheduled-publish executor doesn't need a Payload hook. They're cron / event handlers that happen to read from Payload.
78
100
 
101
+ The tag-revalidation hooks are the one place this package reaches into Payload configuration, and only because you put them there: they are plain hook functions, not a plugin. They live here rather than in `publishing` because this package already owns Next cache invalidation — the optional `next` peer, the Next 16 `revalidateTag` profile, and the tag `createRevalidateOnPublishFunction` fires — and one tag scheme has to serve both. They use publishing's `isDraftWrite`, which makes `publishing` a dependency of this package. Publishing depends on nothing here, so there is no cycle.
102
+
103
+ ## Cache tags: one scheme, both ends
104
+
105
+ A cache tag has two ends: a reader that caches under it and a writer that drops it. Nothing in the type system connects them. Name them differently and everything compiles, every test passes, the hook runs, and the page never refreshes. So build both from one object:
106
+
107
+ ```typescript
108
+ // src/lib/cache-tags.ts — imported by payload.config.ts, the Inngest route and readers
109
+ import { createCacheTags } from '@forumone/throughline-workflows/cache-tags'
110
+
111
+ export const cacheTags = createCacheTags()
112
+ // or name them your way:
113
+ // createCacheTags({ collection: (slug) => `c:${slug}`, global: (slug) => `g:${slug}` })
114
+ ```
115
+
116
+ The defaults are the bare slug for a collection (`pages`, which is also what `createRevalidateOnPublishFunction` has always fired) and `global_<slug>` for a global. The `/cache-tags` subpath imports nothing, so frontend code can use it without pulling in Payload.
117
+
118
+ A reader tags its cached read from the same object:
119
+
120
+ ```typescript
121
+ import { unstable_cache } from 'next/cache'
122
+ import { cacheTags } from '@/lib/cache-tags'
123
+
124
+ export const getNavigation = unstable_cache(
125
+ async () => (await getPayload({ config })).findGlobal({ slug: 'navigation' }),
126
+ ['navigation'],
127
+ { tags: [cacheTags.global('navigation')] },
128
+ )
129
+ // With Cache Components: cacheTag(cacheTags.global('navigation')) inside a 'use cache' function.
130
+ ```
131
+
132
+ ## Tag-revalidation hooks
133
+
134
+ `createRevalidateOnPublishFunction` reacts to publishing events, so it covers publishes, unpublishes and rollbacks of publishable collections — and nothing else. A global (navigation, footer, settings) is read on every page and usually cached, and nothing publishes it. A collection an integration writes emits no publishing event. A delete is not a publish. Each of those leaves the cached copy standing until the next deploy unless a hook drops it:
135
+
136
+ ```typescript
137
+ import { createTagRevalidationHooks } from '@forumone/throughline-workflows'
138
+ import { cacheTags } from './lib/cache-tags'
139
+
140
+ const revalidation = createTagRevalidationHooks({ cacheTags })
141
+
142
+ const Pages: CollectionConfig = {
143
+ slug: 'pages',
144
+ hooks: {
145
+ // Navigation links to pages and caches their slugs, so a page change drops it too.
146
+ afterChange: [
147
+ revalidation.afterCollectionChange({
148
+ tags: (t, slug) => [t.collection(slug), t.global('navigation')],
149
+ }),
150
+ ],
151
+ afterDelete: [revalidation.afterCollectionDelete()],
152
+ },
153
+ // ...
154
+ }
155
+
156
+ const Media: CollectionConfig = {
157
+ slug: 'media',
158
+ upload: true,
159
+ hooks: {
160
+ // An upload cannot be referenced by anything cached yet.
161
+ afterChange: [revalidation.afterCollectionChange({ operations: ['update'] })],
162
+ afterDelete: [revalidation.afterCollectionDelete()],
163
+ },
164
+ // ...
165
+ }
166
+
167
+ const Navigation: GlobalConfig = {
168
+ slug: 'navigation',
169
+ hooks: { afterChange: [revalidation.afterGlobalChange()] },
170
+ // ...
171
+ }
172
+ ```
173
+
174
+ What they do:
175
+
176
+ - **Drop tags, not paths.** Each hook calls `revalidateTag(tag, { expire: 0 })`, the immediate expiry Next 16 requires a profile for. Paths are the workflow's job. A collection hook defaults to `cacheTags.collection(slug)`, a global hook to `cacheTags.global(slug)`; `tags` replaces that list.
177
+ - **Skip draft writes.** On `update`, a collection hook asks publishing's `isDraftWrite`, so a draft save or an autosave tick drops nothing, while an unpublish still does. That answer is recorded by `publishingPlugin` for the collections it manages; on any other collection every write counts as visible, which is the safe direction. A `create` that lands as a draft is skipped too: nothing published existed a moment ago. Global hooks drop on every save.
178
+ - **Never throw.** A throwing `afterChange` fails the editor's save over a cache miss. Outside a Next request — seeds, migrations, the Payload CLI under `tsx` — Next throws `static generation store missing`; there is no server to be stale, so that is logged at `debug`. Any other failure means the cache did not clear while a server was running, and is logged at `error` with the cause and the tag.
179
+ - **Other frontends.** Pass `revalidateTag: (tag) => myCdn.purgeTag(tag)` to drop tags somewhere other than Next.
180
+
181
+ These overlap with `createRevalidateOnPublishFunction` on a publish — both drop the collection tag — which is harmless: dropping a tag twice is dropping it once. Use the workflow for page paths and the sitemap, and the hooks for everything a publish event never announces.
182
+
183
+ ## Migrating from the built-in URL builders
184
+
185
+ Before 0.5, `createRevalidateOnPublishFunction` guessed paths: `pages` → `/<slug>` (`home` → `/`), `posts` → `/blog/<slug>`, and any other collection → `/<slug>`. A site whose routes differed revalidated the wrong path without a word. `urlBuilders` is now required and has no built-in entries. A collection with no entry still has its tags dropped, but no path is revalidated and the run logs a warning.
186
+
187
+ If you relied on the defaults, pass them explicitly — the same behaviour as before:
188
+
189
+ ```typescript
190
+ createRevalidateOnPublishFunction({
191
+ inngest,
192
+ payload,
193
+ urlBuilders: {
194
+ pages: (slug) => (slug === 'home' || slug === '' ? '/' : `/${slug}`),
195
+ posts: (slug) => `/blog/${slug}`,
196
+ },
197
+ })
198
+ ```
199
+
200
+ Then check them against your routes: if your posts are not under `/blog`, they never were being revalidated.
201
+
79
202
  ## Non-Next.js frontends
80
203
 
81
- The default `revalidate` function dynamically imports `next/cache` and is a no-op anywhere `revalidatePath` / `revalidateTag` aren't available. For other frameworks supply your own:
204
+ The default `revalidate` function dynamically imports `next/cache`, so the package installs and imports without Next. For other frameworks supply your own:
82
205
 
83
206
  ```typescript
84
207
  createRevalidateOnPublishFunction({
85
208
  inngest,
86
209
  payload,
210
+ urlBuilders,
87
211
  revalidate: async ({ path, tags }) => {
88
212
  if (path) await myCdn.purge(path)
89
213
  for (const tag of tags) await myCdn.purgeTag(tag)
@@ -93,6 +217,46 @@ createRevalidateOnPublishFunction({
93
217
 
94
218
  `next` is declared as an optional peer (`peerDependenciesMeta.next.optional = true`) so non-Next consumers don't see a missing-peer warning.
95
219
 
220
+ ## Scheduled publishing: sleep, don't poll
221
+
222
+ `createPublishAtScheduledTimeFunction` is woken by `content/page.scheduled` — which
223
+ the publishing plugin sends whenever a document's scheduled time changes — sleeps
224
+ until that time, re-reads the document, and publishes it only if the document still
225
+ carries that exact time. The document is the source of truth and the event is a
226
+ wake-up call, so a reschedule, a cancellation, or a publish or unpublish in the
227
+ meantime needs no cancellation event: the old run wakes, finds the time changed, and
228
+ ends.
229
+
230
+ It publishes on the minute and costs a few steps per schedule. The poller costs two
231
+ executions per tick whether or not anything is due — at `*/15` that is 5,760 a month
232
+ to find, almost always, nothing.
233
+
234
+ A schedule more than six days out is carried in hops: the run sleeps six days,
235
+ re-sends the event and ends, so no single sleep passes the seven-day cap on
236
+ Inngest's free plan and no run approaches its thirty-day limit. Set `maxSleepMs` on
237
+ a paid plan if you prefer fewer hops.
238
+
239
+ Run the poller alongside it as a backstop for an event that never arrived, on a slow
240
+ schedule and with `overdueByMs`, so the two never reach a just-due document in the
241
+ same moment:
242
+
243
+ ```ts
244
+ createPublishAtScheduledTimeFunction({ inngest, payload, collections, publish }),
245
+ createExecuteScheduledPublishesFunction({
246
+ inngest,
247
+ payload,
248
+ collections,
249
+ publish,
250
+ schedule: '17 3 * * *',
251
+ overdueByMs: 60 * 60 * 1000,
252
+ }),
253
+ ```
254
+
255
+ Both read the document's **latest version** (`draft: true`). A schedule is written
256
+ by a draft save, and Payload writes a draft save to the versions table only, so the
257
+ main row never carries it — and reading the versions is also what lets a revision
258
+ of a live document be scheduled.
259
+
96
260
  ## Why scheduled publishes go through the pipeline
97
261
 
98
262
  `createExecuteScheduledPublishesFunction` never writes to Payload. It finds what is
@@ -151,10 +315,12 @@ Each handler runs in its own `step.run`, so failures isolate.
151
315
 
152
316
  ## Options reference
153
317
 
154
- Every factory takes a typed options object. See `src/types.ts` for the full surface — defaults documented there:
318
+ Every factory takes a typed options object. See `src/types.ts` for the full surface — defaults documented there. All of them also accept `onTerminalFailure?` and `concurrency?`:
155
319
 
156
- - `RevalidateOnPublishOptions` — `revalidate?`, `urlBuilders?`, `collectionTags?`, `id?`
157
- - `ExecuteScheduledPublishesOptions` — `collections[]`, `publishingServerUrl`, `publishingApiKey?` (env fallback), `schedule?` (default `*/5 * * * *`), `id?`
320
+ - `RevalidateOnPublishOptions` — `urlBuilders` (required), `cacheTags?`, `collectionTags?`, `revalidate?`, `id?`
321
+ - `TagRevalidationOptions` — `cacheTags?`, `revalidateTag?`; per hook, `tags?` and (collection `afterChange`) `operations?`
322
+ - `PublishAtScheduledTimeOptions` — `collections[]`, `publish`, `maxSleepMs?` (default six days), `id?`
323
+ - `ExecuteScheduledPublishesOptions` — `collections[]`, `publish`, `schedule?` (default `*/5 * * * *`), `overdueByMs?` (default 0), `id?`
158
324
  - `ExpireStaleApprovalsOptions` — `collectionSlug?` (default `approvals`), `schedule?` (default `0 2 * * *`), `id?`
159
325
  - `AuditEventEchoOptions` — `handlers?`, `id?`
160
326
  - `HealthcheckOptions` — `checks[]`, `schedule?` (default `*/15 * * * *`), `onFailure?`, `id?`
@@ -162,6 +328,6 @@ Every factory takes a typed options object. See `src/types.ts` for the full surf
162
328
  ## Related packages
163
329
 
164
330
  - `@forumone/throughline-core` — required peer; provides the audit writer the approval-expiration cron uses
165
- - `@forumone/throughline-publishing` — emits the publishing events the revalidation cron subscribes to
331
+ - `@forumone/throughline-publishing` — emits the publishing events the revalidation workflow subscribes to, and provides the `isDraftWrite` predicate the tag hooks use (a dependency)
166
332
  - `@forumone/throughline-approvals` — owns the approvals collection the expiration cron reads
167
333
  - `@forumone/throughline-email` (C11) — will subscribe to `notification/send-approval-*`
@@ -0,0 +1,31 @@
1
+ /** How a site names its tags. Each builder is optional; omitted ones use the default. */
2
+ export interface CacheTagScheme {
3
+ /**
4
+ * The tag for cached reads derived from a collection's documents.
5
+ * Default: the bare slug (`pages`), which is also what
6
+ * `createRevalidateOnPublishFunction` has always fired.
7
+ */
8
+ collection?: (slug: string) => string;
9
+ /** The tag for a cached global. Default: `global_<slug>`. */
10
+ global?: (slug: string) => string;
11
+ }
12
+ /** The built scheme. Pass the same instance to writers and readers. */
13
+ export interface CacheTags {
14
+ collection(slug: string): string;
15
+ global(slug: string): string;
16
+ }
17
+ /**
18
+ * Builds the tag scheme both ends of the cache use.
19
+ *
20
+ * ```ts
21
+ * // src/lib/cache-tags.ts — imported by payload.config.ts and by readers
22
+ * export const cacheTags = createCacheTags()
23
+ *
24
+ * // a reader
25
+ * unstable_cache(load, ['navigation'], { tags: [cacheTags.global('navigation')] })
26
+ * ```
27
+ */
28
+ export declare function createCacheTags(scheme?: CacheTagScheme): CacheTags;
29
+ /** The scheme every writer uses when it is given none. */
30
+ export declare const defaultCacheTags: CacheTags;
31
+ //# sourceMappingURL=cache-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-tags.d.ts","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"AAqBA,yFAAyF;AACzF,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACrC,6DAA6D;IAC7D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAClC;AAED,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,MAAM,GAAE,cAAmB,GAAG,SAAS,CAOtE;AAED,0DAA0D;AAC1D,eAAO,MAAM,gBAAgB,EAAE,SAA6B,CAAA"}
@@ -0,0 +1,53 @@
1
+ /*
2
+ Next cache tags, named in one place.
3
+
4
+ A tag is one string with two ends: the reader that caches under it
5
+ (`unstable_cache(..., { tags })`, `cacheTag()`, `fetch(..., { next: { tags } })`)
6
+ and the writer that drops it (the hooks in `revalidate-tag-hooks.ts`, and
7
+ `createRevalidateOnPublishFunction`). Nothing in the type system connects the two
8
+ ends. Name them differently and everything compiles, every test passes, the hook
9
+ runs, and the page never refreshes — the first site on this suite shipped a hook
10
+ that had never invalidated anything for exactly that reason.
11
+
12
+ So both ends build tags from one object. A site makes it once, in a module its
13
+ `payload.config.ts` and its frontend both import, and hands it to the hooks and
14
+ the readers alike.
15
+
16
+ Deliberately dependency-free, and published on its own subpath
17
+ (`@forumone/throughline-workflows/cache-tags`): one of its importers is
18
+ `payload.config.ts` and the other is frontend code, and neither should pull in
19
+ the other's world to name a string.
20
+ */
21
+ /**
22
+ * Builds the tag scheme both ends of the cache use.
23
+ *
24
+ * ```ts
25
+ * // src/lib/cache-tags.ts — imported by payload.config.ts and by readers
26
+ * export const cacheTags = createCacheTags()
27
+ *
28
+ * // a reader
29
+ * unstable_cache(load, ['navigation'], { tags: [cacheTags.global('navigation')] })
30
+ * ```
31
+ */
32
+ export function createCacheTags(scheme = {}) {
33
+ const collection = scheme.collection ?? ((slug) => slug);
34
+ const global = scheme.global ?? ((slug) => `global_${slug}`);
35
+ return {
36
+ collection: (slug) => nonEmpty(collection(slug), 'collection', slug),
37
+ global: (slug) => nonEmpty(global(slug), 'global', slug),
38
+ };
39
+ }
40
+ /** The scheme every writer uses when it is given none. */
41
+ export const defaultCacheTags = createCacheTags();
42
+ /*
43
+ An empty tag is the silent version of the mismatch above: `revalidateTag('')`
44
+ does nothing and says nothing. A builder that produces one is a bug in the
45
+ site's configuration, and the place to hear about it is the first call.
46
+ */
47
+ function nonEmpty(tag, kind, slug) {
48
+ if (typeof tag !== 'string' || tag.length === 0) {
49
+ throw new Error(`The cache tag scheme built an empty ${kind} tag for "${slug}".`);
50
+ }
51
+ return tag;
52
+ }
53
+ //# sourceMappingURL=cache-tags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;EAmBE;AAoBF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,SAAyB,EAAE;IACzD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,CAAA;IAChE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,IAAI,EAAE,CAAC,CAAA;IACpE,OAAO;QACL,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC;QACpE,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC;KACzD,CAAA;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAc,eAAe,EAAE,CAAA;AAE5D;;;;EAIE;AACF,SAAS,QAAQ,CAAC,GAAW,EAAE,IAAY,EAAE,IAAY;IACvD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,aAAa,IAAI,IAAI,CAAC,CAAA;IACnF,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
@@ -3,9 +3,13 @@ import type { ExecuteScheduledPublishesOptions } from './types.js';
3
3
  /**
4
4
  * Cron-driven scheduled publish executor. Every tick (default: every 5
5
5
  * minutes) the function looks for documents in any of the configured
6
- * collections whose `_status` is still `draft` and whose
6
+ * collections whose latest version is still a draft and whose
7
7
  * `scheduledPublishAt` has passed, and calls `options.publish` for each.
8
8
  *
9
+ * `createPublishAtScheduledTimeFunction` does the same job on time and without
10
+ * polling. Run alongside it, this is the backstop — set `overdueByMs` and a
11
+ * slow schedule, and it only ever finds a document whose event went missing.
12
+ *
9
13
  * `publish` must go through the publishing pipeline — see the option's own
10
14
  * documentation for the wiring. That is what makes a scheduled publish get the
11
15
  * same composition / accessibility / approval checks as an interactive one.
@@ -1 +1 @@
1
- {"version":3,"file":"execute-scheduled-publishes.d.ts","sourceRoot":"","sources":["../src/execute-scheduled-publishes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAA;AAWlE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,EAAE,gCAAgC,GACxC,eAAe,CAAC,GAAG,CAuFrB"}
1
+ {"version":3,"file":"execute-scheduled-publishes.d.ts","sourceRoot":"","sources":["../src/execute-scheduled-publishes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,YAAY,CAAA;AAWlE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uCAAuC,CACrD,OAAO,EAAE,gCAAgC,GACxC,eAAe,CAAC,GAAG,CAiGrB"}
@@ -4,9 +4,13 @@ const REASONING = 'Scheduled publish executed by workflow cron';
4
4
  /**
5
5
  * Cron-driven scheduled publish executor. Every tick (default: every 5
6
6
  * minutes) the function looks for documents in any of the configured
7
- * collections whose `_status` is still `draft` and whose
7
+ * collections whose latest version is still a draft and whose
8
8
  * `scheduledPublishAt` has passed, and calls `options.publish` for each.
9
9
  *
10
+ * `createPublishAtScheduledTimeFunction` does the same job on time and without
11
+ * polling. Run alongside it, this is the backstop — set `overdueByMs` and a
12
+ * slow schedule, and it only ever finds a document whose event went missing.
13
+ *
10
14
  * `publish` must go through the publishing pipeline — see the option's own
11
15
  * documentation for the wiring. That is what makes a scheduled publish get the
12
16
  * same composition / accessibility / approval checks as an interactive one.
@@ -33,7 +37,7 @@ export function createExecuteScheduledPublishesFunction(options) {
33
37
  ...failureOptions(options, 1),
34
38
  triggers: [{ cron: schedule }],
35
39
  }, async ({ step, logger }) => {
36
- const nowIso = new Date().toISOString();
40
+ const dueBy = new Date(Date.now() - (options.overdueByMs ?? 0)).toISOString();
37
41
  let publishedCount = 0;
38
42
  let blockedCount = 0;
39
43
  for (const config of options.collections) {
@@ -46,9 +50,19 @@ export function createExecuteScheduledPublishesFunction(options) {
46
50
  and: [
47
51
  { [statusField]: { equals: 'draft' } },
48
52
  { [scheduledField]: { exists: true } },
49
- { [scheduledField]: { less_than_equal: nowIso } },
53
+ { [scheduledField]: { less_than_equal: dueBy } },
50
54
  ],
51
55
  },
56
+ /*
57
+ The latest version, not the main row. A schedule is set by a draft
58
+ save, and Payload writes a draft save to the versions table alone —
59
+ so without this the poll read a column that the admin never wrote,
60
+ and only a schedule set over MCP, by a non-draft update, was ever
61
+ found. It is also what lets a revision of a live document be
62
+ scheduled: its latest version is a draft even while the main row
63
+ says `published`.
64
+ */
65
+ draft: true,
52
66
  limit: 100,
53
67
  });
54
68
  return result.docs.map((doc) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"execute-scheduled-publishes.js","sourceRoot":"","sources":["../src/execute-scheduled-publishes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"execute-scheduled-publishes.js","sourceRoot":"","sources":["../src/execute-scheduled-publishes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { Payload } from 'payload';
2
+ import type { Logger } from '@forumone/throughline-core';
3
+ import { type ErrorReporter, type JobFailureWriter } from '@forumone/throughline-core';
4
+ import type { HealthcheckOptions, WorkflowFailureHandler } from './types.js';
5
+ export interface FailureHandlerOptions {
6
+ /**
7
+ * The Payload instance whose `jobFailuresPlugin` writer receives the row.
8
+ * Without one — or without the plugin — the failure is logged and reported
9
+ * but not recorded.
10
+ */
11
+ payload?: Payload | undefined;
12
+ /** Replaces the writer found on `payload`. */
13
+ writer?: JobFailureWriter | undefined;
14
+ /** Default: `reportError` from core, which posts to `ERROR_WEBHOOK_URL`. Pass `false` to not report. */
15
+ report?: ErrorReporter | false | undefined;
16
+ /** Default: core's console logger. */
17
+ logger?: Logger | undefined;
18
+ }
19
+ export interface HealthcheckFailureHandlerOptions extends FailureHandlerOptions {
20
+ /** The healthcheck function's id, recorded as the failure's source. Default: `'healthcheck'`. */
21
+ functionId?: string | undefined;
22
+ }
23
+ /**
24
+ * An `onTerminalFailure` handler for any workflow factory here — or for any
25
+ * Inngest function, as its `onFailure`.
26
+ *
27
+ * ```ts
28
+ * const onTerminalFailure = createTerminalFailureHandler({ payload })
29
+ * createExpireStaleApprovalsFunction({ inngest, payload, onTerminalFailure })
30
+ * ```
31
+ */
32
+ export declare function createTerminalFailureHandler(options?: FailureHandlerOptions): WorkflowFailureHandler;
33
+ /**
34
+ * A `HealthcheckOptions.onFailure` handler: once per run with failing checks,
35
+ * to the same three places.
36
+ */
37
+ export declare function createHealthcheckFailureHandler(options?: HealthcheckFailureHandlerOptions): NonNullable<HealthcheckOptions['onFailure']>;
38
+ //# sourceMappingURL=failure-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-handler.d.ts","sourceRoot":"","sources":["../src/failure-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EAOL,KAAK,aAAa,EAGlB,KAAK,gBAAgB,EACtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAuB5E,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAA;IACrC,wGAAwG;IACxG,MAAM,CAAC,EAAE,aAAa,GAAG,KAAK,GAAG,SAAS,CAAA;IAC1C,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC5B;AAED,MAAM,WAAW,gCAAiC,SAAQ,qBAAqB;IAC7E,iGAAiG;IACjG,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,qBAA0B,GAClC,sBAAsB,CAgBxB;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,gCAAqC,GAC7C,WAAW,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC,CAa9C"}
@@ -0,0 +1,60 @@
1
+ import { buildHealthcheckFailureReport, buildJobFailureReport, defaultLogger, getJobFailureWriter, reportError, summariseReport, } from '@forumone/throughline-core';
2
+ /**
3
+ * An `onTerminalFailure` handler for any workflow factory here — or for any
4
+ * Inngest function, as its `onFailure`.
5
+ *
6
+ * ```ts
7
+ * const onTerminalFailure = createTerminalFailureHandler({ payload })
8
+ * createExpireStaleApprovalsFunction({ inngest, payload, onTerminalFailure })
9
+ * ```
10
+ */
11
+ export function createTerminalFailureHandler(options = {}) {
12
+ return async ({ error, event }) => {
13
+ let report;
14
+ try {
15
+ const data = event?.data;
16
+ report = buildJobFailureReport({
17
+ functionId: data?.function_id,
18
+ runId: data?.run_id,
19
+ triggerEvent: data?.event?.name,
20
+ error,
21
+ });
22
+ }
23
+ catch {
24
+ return;
25
+ }
26
+ await dispatch(report, '[job-failed]', options);
27
+ };
28
+ }
29
+ /**
30
+ * A `HealthcheckOptions.onFailure` handler: once per run with failing checks,
31
+ * to the same three places.
32
+ */
33
+ export function createHealthcheckFailureHandler(options = {}) {
34
+ return async (failures) => {
35
+ let report;
36
+ try {
37
+ report = buildHealthcheckFailureReport(failures, options.functionId ? { functionId: options.functionId } : {});
38
+ }
39
+ catch {
40
+ return;
41
+ }
42
+ await dispatch(report, '[healthcheck-failed]', options);
43
+ };
44
+ }
45
+ async function dispatch(report, tag, options) {
46
+ const logger = options.logger ?? defaultLogger;
47
+ try {
48
+ // The stack goes to the log and the webhook; the row keeps the message.
49
+ logger.error(`${tag} ${summariseReport(report)}`, { report });
50
+ }
51
+ catch {
52
+ // Carry on to the other two.
53
+ }
54
+ const writer = options.writer ?? (options.payload ? getJobFailureWriter(options.payload) : undefined);
55
+ const reporter = options.report === false ? undefined : (options.report ?? reportError);
56
+ // Core's writer and reporter never reject. These wrappers are for a host's
57
+ // own, which may not keep that promise — or may throw before returning one.
58
+ await Promise.allSettled([(async () => writer?.(report))(), (async () => reporter?.(report))()]);
59
+ }
60
+ //# sourceMappingURL=failure-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure-handler.js","sourceRoot":"","sources":["../src/failure-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,6BAA6B,EAC7B,qBAAqB,EACrB,aAAa,EACb,mBAAmB,EACnB,WAAW,EACX,eAAe,GAKhB,MAAM,4BAA4B,CAAA;AA4CnC;;;;;;;;GAQG;AACH,MAAM,UAAU,4BAA4B,CAC1C,UAAiC,EAAE;IAEnC,OAAO,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;QAChC,IAAI,MAAwB,CAAA;QAC5B,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI,CAAA;YACxB,MAAM,GAAG,qBAAqB,CAAC;gBAC7B,UAAU,EAAE,IAAI,EAAE,WAAW;gBAC7B,KAAK,EAAE,IAAI,EAAE,MAAM;gBACnB,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;gBAC/B,KAAK;aACN,CAAC,CAAA;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QACD,MAAM,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,CAAC,CAAA;IACjD,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,+BAA+B,CAC7C,UAA4C,EAAE;IAE9C,OAAO,KAAK,EAAE,QAAQ,EAAE,EAAE;QACxB,IAAI,MAAgC,CAAA;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,6BAA6B,CACpC,QAAQ,EACR,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAC7D,CAAA;QACH,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QACD,MAAM,QAAQ,CAAC,MAAM,EAAE,sBAAsB,EAAE,OAAO,CAAC,CAAA;IACzD,CAAC,CAAA;AACH,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,MAAmD,EACnD,GAAW,EACX,OAA8B;IAE9B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,aAAa,CAAA;IAC9C,IAAI,CAAC;QACH,wEAAwE;QACxE,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,6BAA6B;IAC/B,CAAC;IAED,MAAM,MAAM,GACV,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,mBAAmB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IACxF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC,CAAA;IAEvF,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAClG,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  export { createRevalidateOnPublishFunction } from './revalidate-on-publish.js';
2
+ export { createTagRevalidationHooks } from './revalidate-tag-hooks.js';
3
+ export type { CollectionTagHookOptions, GlobalTagHookOptions, RevalidateTagFn, TagRevalidationHooks, TagRevalidationOptions, TagSelector, } from './revalidate-tag-hooks.js';
4
+ export { createCacheTags, defaultCacheTags } from './cache-tags.js';
5
+ export type { CacheTags, CacheTagScheme } from './cache-tags.js';
2
6
  export { createExecuteScheduledPublishesFunction } from './execute-scheduled-publishes.js';
7
+ export { createPublishAtScheduledTimeFunction } from './publish-at-scheduled-time.js';
3
8
  export { createExpireStaleApprovalsFunction } from './expire-stale-approvals.js';
4
9
  export { createAuditEventEchoFunction } from './audit-event-echo.js';
5
10
  export { createHealthcheckFunction, createPayloadReachableCheck, createManifestReachableCheck, } from './healthcheck.js';
6
11
  export { failureOptions } from './types.js';
7
- export type { BaseWorkflowOptions, WorkflowFailureHandler, FailureAwareOptions, RevalidateFn, RevalidatePathsInput, RevalidateOnPublishOptions, ScheduledCollectionConfig, ExecuteScheduledPublishesOptions, ExpireStaleApprovalsOptions, AuditEchoEvent, AuditEchoHandler, AuditEventEchoOptions, HealthcheckDefinition, HealthcheckOptions, HealthcheckResult, } from './types.js';
12
+ export { createHealthcheckFailureHandler, createTerminalFailureHandler, } from './failure-handler.js';
13
+ export type { FailureHandlerOptions, HealthcheckFailureHandlerOptions, } from './failure-handler.js';
14
+ export type { BaseWorkflowOptions, WorkflowFailureHandler, FailureAwareOptions, RevalidateFn, RevalidatePathsInput, RevalidateOnPublishOptions, ScheduledCollectionConfig, ExecuteScheduledPublishesOptions, PublishAtScheduledTimeOptions, ScheduledPublishRequest, ScheduledPublishResult, ExpireStaleApprovalsOptions, AuditEchoEvent, AuditEchoHandler, AuditEventEchoOptions, HealthcheckDefinition, HealthcheckOptions, HealthcheckResult, } from './types.js';
8
15
  //# sourceMappingURL=index.d.ts.map