refactor: simplify running field hooks (#11372)

Previously, we were quite frequently using `.reduce()` to sequentially run field hooks. This PR replaces them with simple `for` loops, which is less overhead, less code, less confusing and simpler to understand.

Additionally, it refactors `mergeLocaleActions` which previously was unnecessarily complex. They no longer entail async code, thus we no longer have to juggle with promises
This commit is contained in:
Alessio Gravili
2025-02-24 11:37:33 -07:00
committed by GitHub
parent 2477fc6c75
commit dc9e8fa655
10 changed files with 89 additions and 125 deletions

View File

@@ -88,7 +88,7 @@ export type BeforeChangeRichTextHookArgs<
errors?: ValidationFieldError[]
/** Only available in `beforeChange` field hooks */
mergeLocaleActions?: (() => Promise<void>)[]
mergeLocaleActions?: (() => Promise<void> | void)[]
/** A string relating to which operation the field type is currently executing within. */
operation?: 'create' | 'delete' | 'read' | 'update'
/** The sibling data of the document before changes being applied. */

View File

@@ -75,10 +75,8 @@ export const promise = async ({
if (fieldAffectsData(field)) {
// Execute hooks
if (field.hooks?.afterChange) {
await field.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of field.hooks.afterChange) {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -102,7 +100,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
}
}
@@ -242,17 +240,15 @@ export const promise = async ({
throw new MissingEditorProp(field) // while we allow disabling editor functionality, you should not have any richText fields defined if you do not have an editor
}
if (typeof field?.editor === 'function') {
if (typeof field.editor === 'function') {
throw new Error('Attempted to access unsanitized rich text editor.')
}
const editor: RichTextAdapter = field?.editor
const editor: RichTextAdapter = field.editor
if (editor?.hooks?.afterChange?.length) {
await editor.hooks.afterChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of editor.hooks.afterChange) {
const hookedValue = await hook({
collection,
context,
data,
@@ -275,7 +271,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingDoc[field.name] = hookedValue
}
}, Promise.resolve())
}
}
break
}

View File

@@ -237,18 +237,17 @@ export const promise = async ({
if (fieldAffectsData(field)) {
// Execute hooks
if (triggerHooks && field.hooks?.afterRead) {
await field.hooks.afterRead.reduce(async (priorHook, currentHook) => {
await priorHook
for (const hook of field.hooks.afterRead) {
const shouldRunHookOnAllLocales =
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) {
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
(async () => {
const hookedValue = await currentHook({
const localesAndValues = Object.entries(siblingDoc[field.name])
await Promise.all(
localesAndValues.map(async ([localeKey, value]) => {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -273,14 +272,12 @@ export const promise = async ({
})
if (hookedValue !== undefined) {
siblingDoc[field.name][locale] = hookedValue
siblingDoc[field.name][localeKey] = hookedValue
}
})(),
}),
)
await Promise.all(hookPromises)
} else {
const hookedValue = await currentHook({
const hookedValue = await hook({
blockData,
collection,
context,
@@ -308,7 +305,7 @@ export const promise = async ({
siblingDoc[field.name] = hookedValue
}
}
}, Promise.resolve())
}
}
// Execute access control
@@ -677,18 +674,18 @@ export const promise = async ({
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.afterRead?.length) {
await editor.hooks.afterRead.reduce(async (priorHook, currentHook) => {
await priorHook
for (const hook of editor.hooks.afterRead) {
const shouldRunHookOnAllLocales =
fieldShouldBeLocalized({ field, parentIsLocalized }) &&
(locale === 'all' || !flattenLocales) &&
typeof siblingDoc[field.name] === 'object'
if (shouldRunHookOnAllLocales) {
const hookPromises = Object.entries(siblingDoc[field.name]).map(([locale, value]) =>
(async () => {
const hookedValue = await currentHook({
const localesAndValues = Object.entries(siblingDoc[field.name])
await Promise.all(
localesAndValues.map(async ([locale, value]) => {
const hookedValue = await hook({
collection,
context,
currentDepth,
@@ -722,12 +719,10 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingDoc[field.name][locale] = hookedValue
}
})(),
}),
)
await Promise.all(hookPromises)
} else {
const hookedValue = await currentHook({
const hookedValue = await hook({
collection,
context,
currentDepth,
@@ -762,7 +757,7 @@ export const promise = async ({
siblingDoc[field.name] = hookedValue
}
}
}, Promise.resolve())
}
}
break
}

View File

@@ -81,10 +81,9 @@ export const beforeChange = async <T extends JsonObject>({
)
}
await mergeLocaleActions.reduce(async (priorAction, action) => {
await priorAction
for (const action of mergeLocaleActions) {
await action()
}, Promise.resolve())
}
return data
}

View File

@@ -31,7 +31,7 @@ type Args = {
fieldIndex: number
global: null | SanitizedGlobalConfig
id?: number | string
mergeLocaleActions: (() => Promise<void>)[]
mergeLocaleActions: (() => Promise<void> | void)[]
operation: Operation
parentIndexPath: string
parentIsLocalized: boolean
@@ -108,10 +108,8 @@ export const promise = async ({
// Execute hooks
if (field.hooks?.beforeChange) {
await field.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of field.hooks.beforeChange) {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -135,7 +133,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
// Validate
@@ -192,29 +190,21 @@ export const promise = async ({
// Push merge locale action if applicable
if (localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
mergeLocaleActions.push(async () => {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
mergeLocaleActions.push(() => {
const localeData = {}
for (const locale of localization.localeCodes) {
const fieldValue =
locale === req.locale
? siblingData[field.name]
: siblingDocWithLocales?.[field.name]?.[locale]
// const result = await localizedValues
// update locale value if it's not undefined
if (typeof fieldValue !== 'undefined') {
return {
...localizedValues,
[locale]: fieldValue,
localeData[locale] = fieldValue
}
}
return localizedValuesPromise
},
Promise.resolve({}),
)
// If there are locales with data, set the data
if (Object.keys(localeData).length > 0) {
siblingData[field.name] = localeData
@@ -423,10 +413,8 @@ export const promise = async ({
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeChange?.length) {
await editor.hooks.beforeChange.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of editor.hooks.beforeChange) {
const hookedValue = await hook({
collection,
context,
data,
@@ -453,7 +441,7 @@ export const promise = async ({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
break

View File

@@ -28,7 +28,7 @@ type Args = {
fields: (Field | TabAsField)[]
global: null | SanitizedGlobalConfig
id?: number | string
mergeLocaleActions: (() => Promise<void>)[]
mergeLocaleActions: (() => Promise<void> | void)[]
operation: Operation
parentIndexPath: string
/**

View File

@@ -6,7 +6,6 @@ import type { Block, Field, FieldHookArgs, TabAsField } from '../../config/types
import { fieldAffectsData, fieldShouldBeLocalized } from '../../config/types.js'
import { getFieldPathsModified as getFieldPaths } from '../../getFieldPaths.js'
import { runBeforeDuplicateHooks } from './runHook.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
@@ -68,10 +67,9 @@ export const promise = async <T>({
// Run field beforeDuplicate hooks
if (Array.isArray(field.hooks?.beforeDuplicate)) {
if (fieldIsLocalized) {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
const localeData: JsonObject = {}
for (const locale of localization.localeCodes) {
const beforeDuplicateArgs: FieldHookArgs = {
blockData,
collection,
@@ -91,20 +89,16 @@ export const promise = async <T>({
value: siblingDoc[field.name]?.[locale],
}
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
let hookResult
for (const hook of field.hooks.beforeDuplicate) {
hookResult = await hook(beforeDuplicateArgs)
}
if (typeof hookResult !== 'undefined') {
return {
...localizedValues,
[locale]: hookResult,
localeData[locale] = hookResult
}
}
return localizedValuesPromise
},
Promise.resolve({}),
)
siblingDoc[field.name] = localeData
} else {
const beforeDuplicateArgs: FieldHookArgs = {
@@ -126,7 +120,11 @@ export const promise = async <T>({
value: siblingDoc[field.name],
}
const hookResult = await runBeforeDuplicateHooks(beforeDuplicateArgs)
let hookResult
for (const hook of field.hooks.beforeDuplicate) {
hookResult = await hook(beforeDuplicateArgs)
}
if (typeof hookResult !== 'undefined') {
siblingDoc[field.name] = hookResult
}

View File

@@ -1,8 +0,0 @@
// @ts-strict-ignore
import type { FieldHookArgs } from '../../config/types.js'
export const runBeforeDuplicateHooks = async (args: FieldHookArgs) =>
await args.field.hooks.beforeDuplicate.reduce(async (priorHook, currentHook) => {
await priorHook
return await currentHook(args)
}, Promise.resolve())

View File

@@ -276,10 +276,8 @@ export const promise = async <T>({
// Execute hooks
if (field.hooks?.beforeValidate) {
await field.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of field.hooks.beforeValidate) {
const hookedValue = await hook({
blockData,
collection,
context,
@@ -303,7 +301,7 @@ export const promise = async <T>({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
// Execute access control
@@ -493,10 +491,8 @@ export const promise = async <T>({
const editor: RichTextAdapter = field?.editor
if (editor?.hooks?.beforeValidate?.length) {
await editor.hooks.beforeValidate.reduce(async (priorHook, currentHook) => {
await priorHook
const hookedValue = await currentHook({
for (const hook of editor.hooks.beforeValidate) {
const hookedValue = await hook({
collection,
context,
data,
@@ -519,7 +515,7 @@ export const promise = async <T>({
if (hookedValue !== undefined) {
siblingData[field.name] = hookedValue
}
}, Promise.resolve())
}
}
break
}

View File

@@ -176,7 +176,7 @@ export type BeforeChangeNodeHookArgs<T extends SerializedLexicalNode> = {
* Only available in `beforeChange` hooks.
*/
errors: ValidationFieldError[]
mergeLocaleActions: (() => Promise<void>)[]
mergeLocaleActions: (() => Promise<void> | void)[]
/** A string relating to which operation the field type is currently executing within. Useful within beforeValidate, beforeChange, and afterChange hooks to differentiate between create and update operations. */
operation: 'create' | 'delete' | 'read' | 'update'
/** The value of the node before any changes. Not available in afterRead hooks */