diff --git a/packages/core/src/actions/context/enhancers/crud/create.ts b/packages/core/src/actions/context/enhancers/crud/create.ts index 00326909..cdb50488 100644 --- a/packages/core/src/actions/context/enhancers/crud/create.ts +++ b/packages/core/src/actions/context/enhancers/crud/create.ts @@ -6,7 +6,6 @@ import changeInstanceExistence from '@foscia/core/actions/context/enhancers/hooks/changeInstanceExistence'; import onRunning from '@foscia/core/actions/context/enhancers/hooks/onRunning'; import onSuccess from '@foscia/core/actions/context/enhancers/hooks/onSuccess'; -import runInstanceHooks from '@foscia/core/actions/context/enhancers/hooks/runInstanceHooks'; import makeEnhancersExtension from '@foscia/core/actions/extensions/makeEnhancersExtension'; import { Action, @@ -16,6 +15,7 @@ import { ConsumeModel, ConsumeSerializer, } from '@foscia/core/actions/types'; +import runHooks from '@foscia/core/hooks/runHooks'; import { Model, ModelClassInstance, ModelInstance } from '@foscia/core/model/types'; /** @@ -39,8 +39,8 @@ export default function create< .use(context({ action: 'create' })) .use(instanceData(instance)) .use(changeInstanceExistence(true)) - .use(onRunning(runInstanceHooks(instance, ['creating', 'saving']))) - .use(onSuccess(runInstanceHooks(instance, ['created', 'saved']))); + .use(onRunning(() => runHooks(instance.$model, ['creating', 'saving'], instance))) + .use(onSuccess(() => runHooks(instance.$model, ['created', 'saved'], instance))); } type EnhancerExtension = ActionParsedExtension<{ diff --git a/packages/core/src/actions/context/enhancers/crud/destroy.ts b/packages/core/src/actions/context/enhancers/crud/destroy.ts index c9d285d6..5e8d7101 100644 --- a/packages/core/src/actions/context/enhancers/crud/destroy.ts +++ b/packages/core/src/actions/context/enhancers/crud/destroy.ts @@ -4,7 +4,6 @@ import changeInstanceExistence from '@foscia/core/actions/context/enhancers/hooks/changeInstanceExistence'; import onRunning from '@foscia/core/actions/context/enhancers/hooks/onRunning'; import onSuccess from '@foscia/core/actions/context/enhancers/hooks/onSuccess'; -import runInstanceHooks from '@foscia/core/actions/context/enhancers/hooks/runInstanceHooks'; import makeEnhancersExtension from '@foscia/core/actions/extensions/makeEnhancersExtension'; import { Action, @@ -13,6 +12,7 @@ import { ConsumeInstance, ConsumeModel, } from '@foscia/core/actions/types'; +import runHooks from '@foscia/core/hooks/runHooks'; import { Model, ModelClassInstance, ModelInstance } from '@foscia/core/model/types'; /** @@ -31,8 +31,8 @@ export default function destroy< .use(forInstance(instance)) .use(context({ action: 'destroy' })) .use(changeInstanceExistence(false)) - .use(onRunning(runInstanceHooks(instance, ['destroying']))) - .use(onSuccess(runInstanceHooks(instance, ['destroyed']))); + .use(onRunning(() => runHooks(instance.$model, ['destroying'], instance))) + .use(onSuccess(() => runHooks(instance.$model, ['destroyed'], instance))); } type EnhancerExtension = ActionParsedExtension<{ diff --git a/packages/core/src/actions/context/enhancers/crud/update.ts b/packages/core/src/actions/context/enhancers/crud/update.ts index e1701f3f..a65912c2 100644 --- a/packages/core/src/actions/context/enhancers/crud/update.ts +++ b/packages/core/src/actions/context/enhancers/crud/update.ts @@ -5,7 +5,6 @@ import changeInstanceExistence from '@foscia/core/actions/context/enhancers/hooks/changeInstanceExistence'; import onRunning from '@foscia/core/actions/context/enhancers/hooks/onRunning'; import onSuccess from '@foscia/core/actions/context/enhancers/hooks/onSuccess'; -import runInstanceHooks from '@foscia/core/actions/context/enhancers/hooks/runInstanceHooks'; import makeEnhancersExtension from '@foscia/core/actions/extensions/makeEnhancersExtension'; import { Action, @@ -15,6 +14,7 @@ import { ConsumeModel, ConsumeSerializer, } from '@foscia/core/actions/types'; +import runHooks from '@foscia/core/hooks/runHooks'; import { Model, ModelClassInstance, ModelInstance } from '@foscia/core/model/types'; /** @@ -37,8 +37,8 @@ export default function update< .use(context({ action: 'update' })) .use(instanceData(instance)) .use(changeInstanceExistence(true)) - .use(onRunning(runInstanceHooks(instance, ['updating', 'saving']))) - .use(onSuccess(runInstanceHooks(instance, ['updated', 'saved']))); + .use(onRunning(() => runHooks(instance.$model, ['updating', 'saving'], instance))) + .use(onSuccess(() => runHooks(instance.$model, ['updated', 'saved'], instance))); } type EnhancerExtension = ActionParsedExtension<{ diff --git a/packages/core/src/actions/context/enhancers/hooks/runInstanceHooks.ts b/packages/core/src/actions/context/enhancers/hooks/runInstanceHooks.ts deleted file mode 100644 index a24fc8ea..00000000 --- a/packages/core/src/actions/context/enhancers/hooks/runInstanceHooks.ts +++ /dev/null @@ -1,14 +0,0 @@ -import runHook from '@foscia/core/hooks/runHook'; -import { ModelHooksDefinition, ModelInstance } from '@foscia/core/model/types'; -import { ArrayableVariadic, sequentialTransform, wrapVariadic } from '@foscia/shared'; - -export default function runInstanceHooks( - instance: ModelInstance, - ...hooks: ArrayableVariadic -) { - return async () => { - await sequentialTransform(wrapVariadic(...hooks).map( - (hook) => () => runHook(instance.$model, hook, instance), - )); - }; -} diff --git a/packages/core/src/actions/makeActionClass.ts b/packages/core/src/actions/makeActionClass.ts index 30b7aef5..0bec92c3 100644 --- a/packages/core/src/actions/makeActionClass.ts +++ b/packages/core/src/actions/makeActionClass.ts @@ -6,7 +6,7 @@ import { ContextRunner, } from '@foscia/core/actions/types'; import registerHook from '@foscia/core/hooks/registerHook'; -import runHook from '@foscia/core/hooks/runHook'; +import runHooks from '@foscia/core/hooks/runHooks'; import { HooksRegistrar } from '@foscia/core/hooks/types'; import withoutHooks from '@foscia/core/hooks/withoutHooks'; import logger from '@foscia/core/logger/logger'; @@ -61,22 +61,22 @@ export default function makeActionClass( public async run(runner: ContextRunner) { const context = await this.useContext(); - await runHook(this, 'running', { context, runner }); + await runHooks(this, 'running', { context, runner }); try { // Context runner might use other context runners, so we must disable // hooks at this point to avoid duplicated hooks runs. const result = await withoutHooks(this, () => runner(this as any)); - await runHook(this, 'success', { context, result }); + await runHooks(this, 'success', { context, result }); return result; } catch (error) { - await runHook(this, 'error', { context, error }); + await runHooks(this, 'error', { context, error }); throw error; } finally { - await runHook(this, 'finally', { context }); + await runHooks(this, 'finally', { context }); } } diff --git a/packages/core/src/hooks/runHook.ts b/packages/core/src/hooks/runHook.ts index 04f5b9dc..7b4f94de 100644 --- a/packages/core/src/hooks/runHook.ts +++ b/packages/core/src/hooks/runHook.ts @@ -1,6 +1,15 @@ import { Hookable, HookCallback, HooksDefinition } from '@foscia/core/hooks/types'; import { sequentialTransform } from '@foscia/shared'; +/** + * Run one hook of a hookable object. + * + * @param hookable + * @param key + * @param event + * + * @deprecated Use {@link runHooks} instead. + */ export default async function runHook( hookable: Hookable, key: K, diff --git a/packages/core/src/hooks/runHooks.ts b/packages/core/src/hooks/runHooks.ts new file mode 100644 index 00000000..e6fe3968 --- /dev/null +++ b/packages/core/src/hooks/runHooks.ts @@ -0,0 +1,23 @@ +import { Hookable, HookCallback, HooksDefinition } from '@foscia/core/hooks/types'; +import { Arrayable, sequentialTransform, wrap } from '@foscia/shared'; + +/** + * Sequentially run multiple hooks of a hookable object. + * + * @param hookable + * @param hooks + * @param event + */ +export default async function runHooks( + hookable: Hookable, + hooks: Arrayable, + event: D[K] extends HookCallback ? E : never, +) { + await sequentialTransform(wrap(hooks).map((hook) => async () => { + const hookCallbacks = hookable.$hooks?.[hook] ?? []; + + await sequentialTransform(hookCallbacks.map((callback) => async () => { + await callback(event); + })); + })); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d6f2e2e5..7197effe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,6 +11,7 @@ import FosciaError from '@foscia/core/errors/fosciaError'; import SerializerError from '@foscia/core/errors/serializerError'; import registerHook from '@foscia/core/hooks/registerHook'; import runHook from '@foscia/core/hooks/runHook'; +import runHooks from '@foscia/core/hooks/runHooks'; import unregisterHook from '@foscia/core/hooks/unregisterHook'; import withoutHooks from '@foscia/core/hooks/withoutHooks'; import logger from '@foscia/core/logger/logger'; @@ -138,6 +139,7 @@ export { restoreSnapshot, takeSnapshot, runHook, + runHooks, registerHook, unregisterHook, withoutHooks, diff --git a/packages/core/tests/unit/hooks/hooks.test.ts b/packages/core/tests/unit/hooks/hooks.test.ts index f0469d8e..a5e1b828 100644 --- a/packages/core/tests/unit/hooks/hooks.test.ts +++ b/packages/core/tests/unit/hooks/hooks.test.ts @@ -1,4 +1,4 @@ -import { Hookable, registerHook, runHook, unregisterHook, withoutHooks } from '@foscia/core'; +import { Hookable, registerHook, runHooks, unregisterHook, withoutHooks } from '@foscia/core'; import { Awaitable } from '@foscia/shared'; import { describe, expect, it, vi } from 'vitest'; @@ -16,7 +16,7 @@ describe.concurrent('unit: hooks', () => { dummyValue = `${dummyValue}>${value}2`; }); - await runHook(dummyHookable, 'dummy', 'foo'); + await runHooks(dummyHookable, 'dummy', 'foo'); expect(dummyValue).toStrictEqual(''); expect(firstHookMock).not.toHaveBeenCalled(); @@ -25,7 +25,7 @@ describe.concurrent('unit: hooks', () => { const unregisterFirst = registerHook(dummyHookable, 'dummy', firstHookMock); registerHook(dummyHookable, 'dummy', secondHookMock); - await runHook(dummyHookable, 'dummy', 'foo'); + await runHooks(dummyHookable, 'dummy', 'foo'); expect(dummyValue).toStrictEqual('>foo1>foo2'); expect(firstHookMock).toHaveBeenCalledOnce(); @@ -36,19 +36,19 @@ describe.concurrent('unit: hooks', () => { // Non-existing callback. }); - await withoutHooks(dummyHookable, () => { + withoutHooks(dummyHookable, () => { registerHook(dummyHookable, 'dummy', firstHookMock); unregisterHook(dummyHookable, 'dummy', secondHookMock); }); - await runHook(dummyHookable, 'dummy', 'foo'); + await runHooks(dummyHookable, 'dummy', 'foo'); expect(dummyValue).toStrictEqual('>foo1>foo2>foo2'); expect(firstHookMock).toHaveBeenCalledOnce(); expect(secondHookMock).toHaveBeenCalledTimes(2); await withoutHooks(dummyHookable, async () => { - await runHook(dummyHookable, 'dummy', 'foo'); + await runHooks(dummyHookable, 'dummy', 'foo'); }); expect(dummyValue).toStrictEqual('>foo1>foo2>foo2'); diff --git a/packages/serialization/src/makeDeserializerWith.ts b/packages/serialization/src/makeDeserializerWith.ts index 6c3eba14..c3468f31 100644 --- a/packages/serialization/src/makeDeserializerWith.ts +++ b/packages/serialization/src/makeDeserializerWith.ts @@ -16,7 +16,7 @@ import { ModelInstance, ModelRelation, normalizeKey, - runHook, + runHooks, shouldSync, } from '@foscia/core'; import { @@ -231,7 +231,7 @@ export default function makeDeserializerWith< instance.$raw = record.raw; markSynced(instance); - await runHook(instance.$model, 'retrieved', instance); + await runHooks(instance.$model, ['retrieved'], instance); const cache = await consumeCache(context, null); if (cache && !isNil(instance.id)) { diff --git a/website/docs/core-concepts/actions.mdx b/website/docs/core-concepts/actions.mdx index 1ed7c26e..d3b3132e 100644 --- a/website/docs/core-concepts/actions.mdx +++ b/website/docs/core-concepts/actions.mdx @@ -317,10 +317,8 @@ parallelized). ::: You can temporally disable hook execution for a given action by using the -`withoutHooks` function. - -Be aware that `withoutHooks` will always return a promise, even when your -callback is a sync function. +`withoutHooks` function. `withoutHooks` can receive a sync or async +callback: if an async callback is passed, it will return a `Promise`. ```typescript import { withoutHooks } from '@foscia/core'; @@ -333,7 +331,7 @@ const users = await withoutHooks(action(), async (a) => { :::warning -Foscia may also register hooks internally when using some enhancers. Those +**Foscia may also register hooks internally** when using some enhancers. Those provide some library features ([**models hooks**](/docs/core-concepts/models#hooks), etc.). Be careful running actions without hooks, as those hooks will also be disable.