@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.
- package/CHANGELOG.md +81 -0
- package/README.md +176 -10
- package/dist/cache-tags.d.ts +31 -0
- package/dist/cache-tags.d.ts.map +1 -0
- package/dist/cache-tags.js +53 -0
- package/dist/cache-tags.js.map +1 -0
- package/dist/execute-scheduled-publishes.d.ts +5 -1
- package/dist/execute-scheduled-publishes.d.ts.map +1 -1
- package/dist/execute-scheduled-publishes.js +17 -3
- package/dist/execute-scheduled-publishes.js.map +1 -1
- package/dist/failure-handler.d.ts +38 -0
- package/dist/failure-handler.d.ts.map +1 -0
- package/dist/failure-handler.js +60 -0
- package/dist/failure-handler.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/next-revalidate.d.ts +43 -0
- package/dist/next-revalidate.d.ts.map +1 -0
- package/dist/next-revalidate.js +42 -0
- package/dist/next-revalidate.js.map +1 -0
- package/dist/publish-at-scheduled-time.d.ts +23 -0
- package/dist/publish-at-scheduled-time.d.ts.map +1 -0
- package/dist/publish-at-scheduled-time.js +133 -0
- package/dist/publish-at-scheduled-time.js.map +1 -0
- package/dist/revalidate-on-publish.d.ts +10 -0
- package/dist/revalidate-on-publish.d.ts.map +1 -1
- package/dist/revalidate-on-publish.js +21 -25
- package/dist/revalidate-on-publish.js.map +1 -1
- package/dist/revalidate-tag-hooks.d.ts +80 -0
- package/dist/revalidate-tag-hooks.d.ts.map +1 -0
- package/dist/revalidate-tag-hooks.js +116 -0
- package/dist/revalidate-tag-hooks.js.map +1 -0
- package/dist/types.d.ts +50 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- 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
|
-
| `
|
|
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.
|
|
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({
|
|
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
|
|
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` — `
|
|
157
|
-
- `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAI3C,MAAM,gBAAgB,GAAG,aAAa,CAAA;AACtC,MAAM,SAAS,GAAG,6CAA6C,CAAA;AAQ/D
|
|
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
|
|
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
|