@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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAA;AAC1F,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C,YAAY,EACV,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,gCAAgC,EAChC,2BAA2B,EAC3B,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AACtE,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,WAAW,GACZ,MAAM,2BAA2B,CAAA;AAKlC,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACnE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChE,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAA;AAC1F,OAAO,EAAE,oCAAoC,EAAE,MAAM,gCAAgC,CAAA;AACrF,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EACL,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,qBAAqB,EACrB,gCAAgC,GACjC,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EACV,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,YAAY,EACZ,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,gCAAgC,EAChC,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,EACtB,2BAA2B,EAC3B,cAAc,EACd,gBAAgB,EAChB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,8 +1,15 @@
1
- // Other factory exports land in subsequent commits.
2
1
  export { createRevalidateOnPublishFunction } from './revalidate-on-publish.js';
2
+ export { createTagRevalidationHooks } from './revalidate-tag-hooks.js';
3
+ /*
4
+ Also on `@forumone/throughline-workflows/cache-tags`, which imports nothing —
5
+ readers in frontend code should import it from there.
6
+ */
7
+ export { createCacheTags, defaultCacheTags } from './cache-tags.js';
3
8
  export { createExecuteScheduledPublishesFunction } from './execute-scheduled-publishes.js';
9
+ export { createPublishAtScheduledTimeFunction } from './publish-at-scheduled-time.js';
4
10
  export { createExpireStaleApprovalsFunction } from './expire-stale-approvals.js';
5
11
  export { createAuditEventEchoFunction } from './audit-event-echo.js';
6
12
  export { createHealthcheckFunction, createPayloadReachableCheck, createManifestReachableCheck, } from './healthcheck.js';
7
13
  export { failureOptions } from './types.js';
14
+ export { createHealthcheckFailureHandler, createTerminalFailureHandler, } from './failure-handler.js';
8
15
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AAEpD,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAA;AAC1F,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iCAAiC,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAA;AAStE;;;EAGE;AACF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEnE,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAA;AAC1F,OAAO,EAAE,oCAAoC,EAAE,MAAM,gCAAgC,CAAA;AACrF,OAAO,EAAE,kCAAkC,EAAE,MAAM,6BAA6B,CAAA;AAChF,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,4BAA4B,GAC7B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EACL,+BAA+B,EAC/B,4BAA4B,GAC7B,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,43 @@
1
+ import type * as NextCacheModule from 'next/cache';
2
+ type NextCache = typeof NextCacheModule;
3
+ /**
4
+ * How stale a caller may find a tag after it is dropped: not at all.
5
+ *
6
+ * Next 16 made `revalidateTag`'s second argument required — it is a cache-life
7
+ * profile — and one-argument calls log a deprecation warning on every call.
8
+ * `{ expire: 0 }` is the immediate expiry the one-argument form used to mean,
9
+ * and it is what an editor pressing Publish or Save expects.
10
+ *
11
+ * Passed unconditionally rather than behind a version check. The peer range is
12
+ * `next >= 15`, and Next 15's `revalidateTag` takes one parameter and ignores a
13
+ * second, so the two-argument call is correct on both.
14
+ *
15
+ * `updateTag`, which Next offers as the other way out of the deprecation, is
16
+ * not usable here: it throws outside a Server Action, and these run in an
17
+ * Inngest step or in Payload's route handler.
18
+ */
19
+ export declare const IMMEDIATE: {
20
+ readonly expire: 0;
21
+ };
22
+ /**
23
+ * `next/cache`, loaded on first use.
24
+ *
25
+ * Dynamic so the package can be imported where Next is not installed (test
26
+ * runners, a non-Next frontend that passes its own revalidator). `next` is an
27
+ * optional peer.
28
+ */
29
+ export declare function loadNextCache(): Promise<NextCache>;
30
+ /**
31
+ * Whether `error` is Next saying there is no request to revalidate for.
32
+ *
33
+ * Seeds, migrations and the Payload CLI write through the same hooks under
34
+ * `tsx` rather than inside a Next server. `revalidateTag` then throws
35
+ * `Invariant: static generation store missing in revalidateTag ...`. Nothing is
36
+ * stale in that case — there is no server to be stale — so it is the one
37
+ * failure that is not a fault.
38
+ *
39
+ * Matched on the message, the only stable surface Next gives it.
40
+ */
41
+ export declare function isOutsideNextRequest(error: unknown): boolean;
42
+ export {};
43
+ //# sourceMappingURL=next-revalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-revalidate.d.ts","sourceRoot":"","sources":["../src/next-revalidate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,eAAe,MAAM,YAAY,CAAA;AAElD,KAAK,SAAS,GAAG,OAAO,eAAe,CAAA;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,SAAS;;CAAyB,CAAA;AAE/C;;;;;;GAMG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,SAAS,CAAC,CAExD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * How stale a caller may find a tag after it is dropped: not at all.
3
+ *
4
+ * Next 16 made `revalidateTag`'s second argument required — it is a cache-life
5
+ * profile — and one-argument calls log a deprecation warning on every call.
6
+ * `{ expire: 0 }` is the immediate expiry the one-argument form used to mean,
7
+ * and it is what an editor pressing Publish or Save expects.
8
+ *
9
+ * Passed unconditionally rather than behind a version check. The peer range is
10
+ * `next >= 15`, and Next 15's `revalidateTag` takes one parameter and ignores a
11
+ * second, so the two-argument call is correct on both.
12
+ *
13
+ * `updateTag`, which Next offers as the other way out of the deprecation, is
14
+ * not usable here: it throws outside a Server Action, and these run in an
15
+ * Inngest step or in Payload's route handler.
16
+ */
17
+ export const IMMEDIATE = { expire: 0 };
18
+ /**
19
+ * `next/cache`, loaded on first use.
20
+ *
21
+ * Dynamic so the package can be imported where Next is not installed (test
22
+ * runners, a non-Next frontend that passes its own revalidator). `next` is an
23
+ * optional peer.
24
+ */
25
+ export async function loadNextCache() {
26
+ return await import('next/cache');
27
+ }
28
+ /**
29
+ * Whether `error` is Next saying there is no request to revalidate for.
30
+ *
31
+ * Seeds, migrations and the Payload CLI write through the same hooks under
32
+ * `tsx` rather than inside a Next server. `revalidateTag` then throws
33
+ * `Invariant: static generation store missing in revalidateTag ...`. Nothing is
34
+ * stale in that case — there is no server to be stale — so it is the one
35
+ * failure that is not a fault.
36
+ *
37
+ * Matched on the message, the only stable surface Next gives it.
38
+ */
39
+ export function isOutsideNextRequest(error) {
40
+ return error instanceof Error && error.message.includes('static generation store missing');
41
+ }
42
+ //# sourceMappingURL=next-revalidate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-revalidate.js","sourceRoot":"","sources":["../src/next-revalidate.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,CAAC,EAAW,CAAA;AAE/C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,OAAO,MAAM,MAAM,CAAC,YAAY,CAAC,CAAA;AACnC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,OAAO,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,iCAAiC,CAAC,CAAA;AAC5F,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { InngestFunction } from 'inngest';
2
+ import type { PublishAtScheduledTimeOptions } from './types.js';
3
+ /**
4
+ * Publishes a document at the moment it was scheduled for, by sleeping until
5
+ * then — rather than polling for due documents.
6
+ *
7
+ * Triggered by `content/page.scheduled`, which the publishing plugin sends
8
+ * whenever a document's scheduled time changes to a new value. The event is a
9
+ * wake-up call and nothing more: **the document is the source of truth.** On
10
+ * waking, the run re-reads the document's latest version and publishes only if
11
+ * it still carries this exact time. So a reschedule, a cancellation, a manual
12
+ * publish in the meantime or an unpublish — each of which changes or clears
13
+ * the field — leaves the old run to wake, find nothing to do, and end. No
14
+ * cancellation event has to arrive for that to be correct, and a duplicate
15
+ * event cannot publish twice (the `idempotency` key drops the second anyway).
16
+ *
17
+ * `publish` must go through the publishing pipeline, exactly as for
18
+ * `createExecuteScheduledPublishesFunction`: a scheduled publish gets the same
19
+ * composition / accessibility / approval checks as an interactive one, and a
20
+ * refusal is logged, not retried.
21
+ */
22
+ export declare function createPublishAtScheduledTimeFunction(options: PublishAtScheduledTimeOptions): InngestFunction.Any;
23
+ //# sourceMappingURL=publish-at-scheduled-time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-at-scheduled-time.d.ts","sourceRoot":"","sources":["../src/publish-at-scheduled-time.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAE,6BAA6B,EAA6B,MAAM,YAAY,CAAA;AA8B1F;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,6BAA6B,GACrC,eAAe,CAAC,GAAG,CA4FrB"}
@@ -0,0 +1,133 @@
1
+ import { failureOptions } from './types.js';
2
+ const SCHEDULED_EVENT = 'content/page.scheduled';
3
+ const REASONING = 'Scheduled publish executed at its scheduled time';
4
+ /*
5
+ Six days, under the seven Inngest's free plan allows a single sleep.
6
+
7
+ A schedule further out than one hop is carried by a relay: the run sleeps a
8
+ hop, then re-sends the event and ends, so each leg is its own run. That also
9
+ keeps every run far inside the free plan's thirty-day run duration, which a
10
+ loop of sleeps inside one run would not — a post scheduled for next quarter
11
+ would simply be killed.
12
+ */
13
+ const DEFAULT_MAX_SLEEP_MS = 6 * 24 * 60 * 60 * 1000;
14
+ /**
15
+ * Publishes a document at the moment it was scheduled for, by sleeping until
16
+ * then — rather than polling for due documents.
17
+ *
18
+ * Triggered by `content/page.scheduled`, which the publishing plugin sends
19
+ * whenever a document's scheduled time changes to a new value. The event is a
20
+ * wake-up call and nothing more: **the document is the source of truth.** On
21
+ * waking, the run re-reads the document's latest version and publishes only if
22
+ * it still carries this exact time. So a reschedule, a cancellation, a manual
23
+ * publish in the meantime or an unpublish — each of which changes or clears
24
+ * the field — leaves the old run to wake, find nothing to do, and end. No
25
+ * cancellation event has to arrive for that to be correct, and a duplicate
26
+ * event cannot publish twice (the `idempotency` key drops the second anyway).
27
+ *
28
+ * `publish` must go through the publishing pipeline, exactly as for
29
+ * `createExecuteScheduledPublishesFunction`: a scheduled publish gets the same
30
+ * composition / accessibility / approval checks as an interactive one, and a
31
+ * refusal is logged, not retried.
32
+ */
33
+ export function createPublishAtScheduledTimeFunction(options) {
34
+ const maxSleepMs = options.maxSleepMs ?? DEFAULT_MAX_SLEEP_MS;
35
+ const collections = new Map(options.collections.map((c) => [c.slug, c]));
36
+ return options.inngest.createFunction({
37
+ id: options.id ?? 'publish-at-scheduled-time',
38
+ /*
39
+ One run per schedule. The plugin sends once per change, but an MCP
40
+ schedule and the field hook seeing the same write, or a retried send,
41
+ would otherwise start two sleepers that both wake and both publish.
42
+ Inngest keeps this key for 24 hours, and a relay is six days after the
43
+ event that started its leg, so a relay is never mistaken for a duplicate.
44
+ */
45
+ idempotency: 'event.data.collection + ":" + event.data.id + ":" + event.data.scheduledFor',
46
+ ...failureOptions(options),
47
+ triggers: [{ event: SCHEDULED_EVENT }],
48
+ }, async ({ event, step, logger }) => {
49
+ const data = event.data;
50
+ const config = data.collection ? collections.get(data.collection) : undefined;
51
+ const target = typeof data.scheduledFor === 'string' ? Date.parse(data.scheduledFor) : NaN;
52
+ if (!config || !data.id || !data.scheduledFor || Number.isNaN(target)) {
53
+ return { outcome: 'ignored', reason: 'not a scheduled publish this function handles' };
54
+ }
55
+ const request = { collection: config.slug, id: data.id, scheduledFor: data.scheduledFor };
56
+ /*
57
+ Measured from the event's own timestamp, not the clock. Inngest replays
58
+ the handler from the top after every step, so a `Date.now()` here would
59
+ give a different answer on each replay — and flipping from the relay
60
+ branch to the direct branch halfway through a run is a run whose steps
61
+ no longer match its history.
62
+ */
63
+ const startedAt = typeof event.ts === 'number' ? event.ts : Date.now();
64
+ if (target - startedAt > maxSleepMs) {
65
+ const hopUntil = new Date(startedAt + maxSleepMs);
66
+ await step.sleepUntil('wait-one-hop', hopUntil);
67
+ // Only relay a schedule that still stands, or a cancelled one would
68
+ // keep re-sending itself every six days until its date came round.
69
+ const current = await step.run('check-still-scheduled', () => isStillScheduled(options, config, request));
70
+ if (!current)
71
+ return { outcome: 'superseded' };
72
+ await step.sendEvent('relay', { name: SCHEDULED_EVENT, data: request });
73
+ return { outcome: 'relayed', hopUntil: hopUntil.toISOString() };
74
+ }
75
+ await step.sleepUntil('wait-until-due', new Date(target));
76
+ const due = await step.run('check-still-scheduled', () => isStillScheduled(options, config, request));
77
+ if (!due) {
78
+ logger.info('Scheduled publish superseded', request);
79
+ return { outcome: 'superseded' };
80
+ }
81
+ return step.run('publish', async () => {
82
+ try {
83
+ const result = await options.publish({
84
+ collection: config.slug,
85
+ id: request.id,
86
+ reasoning: REASONING,
87
+ });
88
+ if (!result.published) {
89
+ logger.warn('Scheduled publish blocked by policy', {
90
+ ...request,
91
+ reason: result.reason,
92
+ });
93
+ return { outcome: 'blocked', reason: result.reason };
94
+ }
95
+ logger.info('Scheduled publish succeeded', request);
96
+ return { outcome: 'published' };
97
+ }
98
+ catch (error) {
99
+ /*
100
+ Not rethrown. A retry would re-run the pipeline against the same
101
+ document, and the daily backstop in
102
+ `createExecuteScheduledPublishesFunction` already comes back for a
103
+ document that is still a draft past its time.
104
+ */
105
+ const reason = error instanceof Error ? error.message : String(error);
106
+ logger.error('Scheduled publish threw', { ...request, error: reason });
107
+ return { outcome: 'error', reason };
108
+ }
109
+ });
110
+ });
111
+ }
112
+ /**
113
+ * Whether the document's latest version still carries this schedule.
114
+ *
115
+ * `draft: true`, because a schedule is written by a draft save and Payload
116
+ * writes a draft save to the versions table only — the main row is not
117
+ * touched, so a read without it would see a document with no schedule at all.
118
+ */
119
+ async function isStillScheduled(options, config, request) {
120
+ const field = config.scheduledField ?? 'scheduledPublishAt';
121
+ const doc = (await options.payload.findByID({
122
+ collection: config.slug,
123
+ id: request.id,
124
+ draft: true,
125
+ depth: 0,
126
+ disableErrors: true,
127
+ }));
128
+ const value = doc?.[field];
129
+ if (typeof value !== 'string')
130
+ return false;
131
+ return Date.parse(value) === Date.parse(request.scheduledFor);
132
+ }
133
+ //# sourceMappingURL=publish-at-scheduled-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-at-scheduled-time.js","sourceRoot":"","sources":["../src/publish-at-scheduled-time.ts"],"names":[],"mappings":"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"}
@@ -5,6 +5,16 @@ import type { RevalidateOnPublishOptions } from './types.js';
5
5
  * publishing taxonomy and revalidates Next.js cache entries for the affected
6
6
  * page, the listing routes, and the sitemap.
7
7
  *
8
+ * The page path comes from `options.urlBuilders`, which is required: only the
9
+ * site knows where a collection's documents are served. A collection with no
10
+ * builder gets its tags dropped and no path revalidated, with a warning —
11
+ * there used to be built-in builders (`pages` → `/<slug>`, `posts` →
12
+ * `/blog/<slug>`, anything else → `/<slug>`), and a site whose routes differed
13
+ * revalidated the wrong path without a word.
14
+ *
15
+ * Tags default to `cacheTags.collection(<collection>)`. Pass the same
16
+ * `cacheTags` your readers and your tag hooks use.
17
+ *
8
18
  * The default revalidate function dynamically imports `next/cache`, so the
9
19
  * package is safe to install in non-Next.js contexts. Pass `options.revalidate`
10
20
  * to use a different cache invalidation strategy.
@@ -1 +1 @@
1
- {"version":3,"file":"revalidate-on-publish.d.ts","sourceRoot":"","sources":["../src/revalidate-on-publish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,KAAK,EAAgB,0BAA0B,EAAwB,MAAM,YAAY,CAAA;AAOhG;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,0BAA0B,GAClC,eAAe,CAAC,GAAG,CA6CrB"}
1
+ {"version":3,"file":"revalidate-on-publish.d.ts","sourceRoot":"","sources":["../src/revalidate-on-publish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAG9C,OAAO,KAAK,EAAgB,0BAA0B,EAAwB,MAAM,YAAY,CAAA;AAEhG;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,0BAA0B,GAClC,eAAe,CAAC,GAAG,CAqDrB"}
@@ -1,19 +1,28 @@
1
1
  import { failureOptions } from './types.js';
2
- const DEFAULT_URL_BUILDERS = {
3
- pages: (slug) => (slug === 'home' || slug === '' ? '/' : `/${slug}`),
4
- posts: (slug) => `/blog/${slug}`,
5
- };
2
+ import { defaultCacheTags } from './cache-tags.js';
3
+ import { IMMEDIATE, loadNextCache } from './next-revalidate.js';
6
4
  /**
7
5
  * Builds the `revalidate-on-publish` Inngest function. Subscribes to the
8
6
  * publishing taxonomy and revalidates Next.js cache entries for the affected
9
7
  * page, the listing routes, and the sitemap.
10
8
  *
9
+ * The page path comes from `options.urlBuilders`, which is required: only the
10
+ * site knows where a collection's documents are served. A collection with no
11
+ * builder gets its tags dropped and no path revalidated, with a warning —
12
+ * there used to be built-in builders (`pages` → `/<slug>`, `posts` →
13
+ * `/blog/<slug>`, anything else → `/<slug>`), and a site whose routes differed
14
+ * revalidated the wrong path without a word.
15
+ *
16
+ * Tags default to `cacheTags.collection(<collection>)`. Pass the same
17
+ * `cacheTags` your readers and your tag hooks use.
18
+ *
11
19
  * The default revalidate function dynamically imports `next/cache`, so the
12
20
  * package is safe to install in non-Next.js contexts. Pass `options.revalidate`
13
21
  * to use a different cache invalidation strategy.
14
22
  */
15
23
  export function createRevalidateOnPublishFunction(options) {
16
- const urlBuilders = { ...DEFAULT_URL_BUILDERS, ...options.urlBuilders };
24
+ const urlBuilders = options.urlBuilders;
25
+ const cacheTags = options.cacheTags ?? defaultCacheTags;
17
26
  const collectionTags = options.collectionTags ?? {};
18
27
  const revalidate = options.revalidate ?? defaultRevalidate;
19
28
  return options.inngest.createFunction({
@@ -34,9 +43,13 @@ export function createRevalidateOnPublishFunction(options) {
34
43
  const data = (event.data ?? {});
35
44
  const collection = data.collection ?? 'pages';
36
45
  const slug = data.slug ?? data.id ?? '';
37
- const tags = collectionTags[collection] ?? [collection];
46
+ const tags = collectionTags[collection] ?? [cacheTags.collection(collection)];
38
47
  await step.run('revalidate-page-path', async () => {
39
- const builder = urlBuilders[collection] ?? ((s) => `/${s}`);
48
+ const builder = Object.hasOwn(urlBuilders, collection) ? urlBuilders[collection] : undefined;
49
+ if (!builder) {
50
+ logger.warn('No urlBuilders entry for this collection, so no page path was revalidated. Add one to createRevalidateOnPublishFunction.', { collection, slug });
51
+ return;
52
+ }
40
53
  const path = builder(slug);
41
54
  await revalidate({ path, tags });
42
55
  logger.info('Revalidated page path', { path, tags });
@@ -50,23 +63,6 @@ export function createRevalidateOnPublishFunction(options) {
50
63
  return { collection, slug, revalidated: true };
51
64
  });
52
65
  }
53
- /**
54
- * How stale a caller may find a tag after this runs: not at all.
55
- *
56
- * Next 16 made `revalidateTag`'s second argument required — it is a cache-life
57
- * profile — and one-argument calls log a deprecation warning on every publish.
58
- * `{ expire: 0 }` is the immediate expiry the one-argument form used to mean,
59
- * and it is what an editor pressing Publish expects.
60
- *
61
- * Passed unconditionally rather than behind a version check. The peer range is
62
- * `next >= 15`, and Next 15's `revalidateTag` takes one parameter and ignores a
63
- * second, so the two-argument call is correct on both.
64
- *
65
- * `updateTag`, which Next offers as the other way out of the deprecation, is
66
- * not usable here: it throws outside a Server Action, and this runs in an
67
- * Inngest step behind a route handler.
68
- */
69
- const IMMEDIATE = { expire: 0 };
70
66
  /**
71
67
  * Default revalidator: dynamic-imports `next/cache` so the package can be
72
68
  * imported in environments without Next.js (e.g. test runners that don't
@@ -74,7 +70,7 @@ const IMMEDIATE = { expire: 0 };
74
70
  * because `revalidatePath('')` triggers a noisy Next.js warning.
75
71
  */
76
72
  const defaultRevalidate = async ({ path, tags }) => {
77
- const { revalidatePath, revalidateTag } = (await import('next/cache'));
73
+ const { revalidatePath, revalidateTag } = await loadNextCache();
78
74
  if (path)
79
75
  revalidatePath(path);
80
76
  for (const tag of tags)
@@ -1 +1 @@
1
- {"version":3,"file":"revalidate-on-publish.js","sourceRoot":"","sources":["../src/revalidate-on-publish.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"revalidate-on-publish.js","sourceRoot":"","sources":["../src/revalidate-on-publish.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, GlobalAfterChangeHook } from 'payload';
2
+ import { type CacheTags } from './cache-tags.js';
3
+ /** Drops one tag. Return a promise to have it awaited. */
4
+ export type RevalidateTagFn = (tag: string) => void | Promise<void>;
5
+ export interface TagRevalidationOptions {
6
+ /**
7
+ * The tag scheme. Pass the same object your readers build their tags from —
8
+ * that is the whole point of it. Default: `defaultCacheTags`.
9
+ */
10
+ cacheTags?: CacheTags;
11
+ /**
12
+ * How to drop a tag. Default: Next's `revalidateTag(tag, { expire: 0 })`,
13
+ * loaded from `next/cache` on first use. Supply your own for a non-Next
14
+ * frontend or a CDN.
15
+ */
16
+ revalidateTag?: RevalidateTagFn;
17
+ }
18
+ /** Which tags a hook drops, built from the scheme. */
19
+ export type TagSelector = (cacheTags: CacheTags, slug: string) => readonly string[];
20
+ export interface CollectionTagHookOptions {
21
+ /**
22
+ * The tags to drop. Default: the collection's own tag,
23
+ * `cacheTags.collection(slug)`. Use it to drop a tag a *different* reader
24
+ * owns — a navigation global that links to pages caches the pages' slugs:
25
+ *
26
+ * ```ts
27
+ * tags: (t, slug) => [t.collection(slug), t.global('navigation')]
28
+ * ```
29
+ */
30
+ tags?: TagSelector;
31
+ /**
32
+ * Which `afterChange` operations drop. Default: both. An upload collection
33
+ * can pass `['update']`: nothing can reference a document that did not exist
34
+ * a moment ago, so creating one makes no cached page untrue.
35
+ */
36
+ operations?: ReadonlyArray<'create' | 'update'>;
37
+ }
38
+ export interface GlobalTagHookOptions {
39
+ /** The tags to drop. Default: the global's own tag, `cacheTags.global(slug)`. */
40
+ tags?: TagSelector;
41
+ }
42
+ export interface TagRevalidationHooks {
43
+ /** `afterChange` for a collection. Skips draft writes. */
44
+ afterCollectionChange(options?: CollectionTagHookOptions): CollectionAfterChangeHook;
45
+ /** `afterDelete` for a collection. */
46
+ afterCollectionDelete(options?: Pick<CollectionTagHookOptions, 'tags'>): CollectionAfterDeleteHook;
47
+ /** `afterChange` for a global. */
48
+ afterGlobalChange(options?: GlobalTagHookOptions): GlobalAfterChangeHook;
49
+ }
50
+ /**
51
+ * Builds the tag-revalidation hooks for one tag scheme.
52
+ *
53
+ * ```ts
54
+ * import { createTagRevalidationHooks } from '@forumone/throughline-workflows'
55
+ * import { cacheTags } from './lib/cache-tags'
56
+ *
57
+ * const revalidation = createTagRevalidationHooks({ cacheTags })
58
+ *
59
+ * const Pages: CollectionConfig = {
60
+ * slug: 'pages',
61
+ * hooks: {
62
+ * afterChange: [revalidation.afterCollectionChange()],
63
+ * afterDelete: [revalidation.afterCollectionDelete()],
64
+ * },
65
+ * ...
66
+ * }
67
+ * const Navigation: GlobalConfig = {
68
+ * slug: 'navigation',
69
+ * hooks: { afterChange: [revalidation.afterGlobalChange()] },
70
+ * ...
71
+ * }
72
+ * ```
73
+ *
74
+ * A hook never throws. Failing it would roll back the editor's save over a
75
+ * cache miss. A failure is logged at `error` instead, and the one expected
76
+ * failure — running outside a Next request, as seeds, migrations and the CLI
77
+ * do — at `debug`.
78
+ */
79
+ export declare function createTagRevalidationHooks(options?: TagRevalidationOptions): TagRevalidationHooks;
80
+ //# sourceMappingURL=revalidate-tag-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidate-tag-hooks.d.ts","sourceRoot":"","sources":["../src/revalidate-tag-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,qBAAqB,EAEtB,MAAM,SAAS,CAAA;AAEhB,OAAO,EAAE,KAAK,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAgBlE,0DAA0D;AAC1D,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEnE,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;CAChC;AAED,sDAAsD;AACtD,MAAM,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,KAAK,SAAS,MAAM,EAAE,CAAA;AAEnF,MAAM,WAAW,wBAAwB;IACvC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAA;CAChD;AAED,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,qBAAqB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,yBAAyB,CAAA;IACpF,sCAAsC;IACtC,qBAAqB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,MAAM,CAAC,GAAG,yBAAyB,CAAA;IAClG,kCAAkC;IAClC,iBAAiB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,qBAAqB,CAAA;CACzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,sBAA2B,GACnC,oBAAoB,CA2DtB"}
@@ -0,0 +1,116 @@
1
+ import { isDraftWrite } from '@forumone/throughline-publishing';
2
+ import { defaultCacheTags } from './cache-tags.js';
3
+ import { IMMEDIATE, isOutsideNextRequest, loadNextCache } from './next-revalidate.js';
4
+ /**
5
+ * Builds the tag-revalidation hooks for one tag scheme.
6
+ *
7
+ * ```ts
8
+ * import { createTagRevalidationHooks } from '@forumone/throughline-workflows'
9
+ * import { cacheTags } from './lib/cache-tags'
10
+ *
11
+ * const revalidation = createTagRevalidationHooks({ cacheTags })
12
+ *
13
+ * const Pages: CollectionConfig = {
14
+ * slug: 'pages',
15
+ * hooks: {
16
+ * afterChange: [revalidation.afterCollectionChange()],
17
+ * afterDelete: [revalidation.afterCollectionDelete()],
18
+ * },
19
+ * ...
20
+ * }
21
+ * const Navigation: GlobalConfig = {
22
+ * slug: 'navigation',
23
+ * hooks: { afterChange: [revalidation.afterGlobalChange()] },
24
+ * ...
25
+ * }
26
+ * ```
27
+ *
28
+ * A hook never throws. Failing it would roll back the editor's save over a
29
+ * cache miss. A failure is logged at `error` instead, and the one expected
30
+ * failure — running outside a Next request, as seeds, migrations and the CLI
31
+ * do — at `debug`.
32
+ */
33
+ export function createTagRevalidationHooks(options = {}) {
34
+ const cacheTags = options.cacheTags ?? defaultCacheTags;
35
+ const revalidateTag = options.revalidateTag ?? nextRevalidateTag;
36
+ async function drop(tags, req) {
37
+ for (const tag of new Set(tags)) {
38
+ try {
39
+ await revalidateTag(tag);
40
+ }
41
+ catch (error) {
42
+ /*
43
+ Outside Next there is no server to be stale, so that one is noise.
44
+ Anything else means the cache did *not* clear while a server was
45
+ running — a real fault, so it is logged loudly. It is still not
46
+ thrown: an `afterChange` that throws fails the editor's save.
47
+ */
48
+ if (isOutsideNextRequest(error)) {
49
+ req.payload.logger.debug({ tag }, 'Skipped cache revalidation: not inside a Next request.');
50
+ }
51
+ else {
52
+ req.payload.logger.error({ err: error, tag }, 'Failed to revalidate a cache tag; the cached copy is stale.');
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return {
58
+ afterCollectionChange(hookOptions = {}) {
59
+ const select = hookOptions.tags ?? ((t, slug) => [t.collection(slug)]);
60
+ const operations = new Set(hookOptions.operations ?? ['create', 'update']);
61
+ return async ({ collection, doc, operation, req }) => {
62
+ if (!operations.has(operation))
63
+ return doc;
64
+ if (isUnpublishedWrite(req, collection.slug, doc, operation))
65
+ return doc;
66
+ await drop(select(cacheTags, collection.slug), req);
67
+ return doc;
68
+ };
69
+ },
70
+ afterCollectionDelete(hookOptions = {}) {
71
+ const select = hookOptions.tags ?? ((t, slug) => [t.collection(slug)]);
72
+ return async ({ collection, doc, req }) => {
73
+ await drop(select(cacheTags, collection.slug), req);
74
+ return doc;
75
+ };
76
+ },
77
+ afterGlobalChange(hookOptions = {}) {
78
+ const select = hookOptions.tags ?? ((t, slug) => [t.global(slug)]);
79
+ return async ({ doc, global, req }) => {
80
+ await drop(select(cacheTags, global.slug), req);
81
+ return doc;
82
+ };
83
+ },
84
+ };
85
+ }
86
+ /*
87
+ A write the public cannot see changes nothing cached, so there is nothing to
88
+ drop — and with autosave on, "a draft save" is every few seconds of an open
89
+ editor. Without this, one editing session evicts the tag over and over and every
90
+ request in between pays a cold read for content that did not change.
91
+
92
+ `isDraftWrite` is publishing's own predicate, the one its trust boundary uses.
93
+ An `afterChange` hook cannot answer this for itself: Payload sets
94
+ `data._status = 'draft'` on any `draft: true` update before hooks run, and
95
+ `previousDoc` is the latest *version*, so a draft save of a published document
96
+ and an unpublish of it look the same from in here. The publishing plugin records
97
+ the operation's real `draft` argument in `beforeOperation`. That is recorded for
98
+ the collections the plugin manages; on any other collection `isDraftWrite` is
99
+ false and the hook drops, which is the safe direction.
100
+
101
+ An unpublish is not a draft write and still drops — it is the case that would
102
+ otherwise leave a cached link pointing at a 404.
103
+
104
+ A *create* that lands as a draft is unambiguous on its own: nothing published
105
+ existed a moment ago, so nothing cached can be about it.
106
+ */
107
+ function isUnpublishedWrite(req, collectionSlug, doc, operation) {
108
+ if (operation === 'create')
109
+ return doc?._status === 'draft';
110
+ return isDraftWrite(req, collectionSlug, doc?.id);
111
+ }
112
+ async function nextRevalidateTag(tag) {
113
+ const { revalidateTag } = await loadNextCache();
114
+ revalidateTag(tag, IMMEDIATE);
115
+ }
116
+ //# sourceMappingURL=revalidate-tag-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revalidate-tag-hooks.js","sourceRoot":"","sources":["../src/revalidate-tag-hooks.ts"],"names":[],"mappings":"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"}