@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/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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,
|
|
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;
|
|
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
|
-
|
|
3
|
-
|
|
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 =
|
|
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]
|
|
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 } =
|
|
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":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;
|
|
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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"}
|