87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
/**
|
|
* Phase 06 #22 — Host-side adapter for emitting plugin hooks.
|
|
*
|
|
* Wraps `publishOutboxEvent` + `emitPluginHook` so call-sites in the
|
|
* existing host code (tenant signup, user register, entity CRUD) stay
|
|
* a single line and never touch the outbox-row construction directly.
|
|
*
|
|
* Each emit runs inside a one-shot Prisma transaction so the outbox
|
|
* row is committed atomically with respect to itself. Hook emission is
|
|
* intentionally fire-and-forget against the surrounding business
|
|
* operation — failures are logged but never thrown, because hook
|
|
* delivery must never break tenant signup / user create / row CRUD.
|
|
*
|
|
* Foundation scope: the worker side that fans the single
|
|
* `plugin.hook.<name>` outbox event out to one invocation per
|
|
* installed plugin is part of the same task (commit 8 wires it into
|
|
* apps/worker/src/jobs/plugin-invoke.ts).
|
|
*/
|
|
|
|
import { db } from "~/utils/db.server";
|
|
import {
|
|
publishOutboxEvent,
|
|
type OutboxPublisherTransaction,
|
|
} from "@toolctrl/db/outbox";
|
|
import {
|
|
emitPluginHook,
|
|
PluginHookValidationError,
|
|
type PluginHookDefinition,
|
|
} from "@toolctrl/plugin-runtime";
|
|
import { createScopedLogger } from "~/utils/logger.server";
|
|
|
|
const pluginHookLogger = createScopedLogger("plugin-hook-publisher");
|
|
|
|
/**
|
|
* Emit a plugin hook outside an existing transaction. Use this for the
|
|
* "fire after the business write succeeded" pattern — it owns its own
|
|
* one-statement tx so the outbox row is atomic in itself, and the
|
|
* caller is decoupled from outbox failure modes.
|
|
*/
|
|
export async function emitPluginHookFireAndForget<TPayload>(
|
|
def: PluginHookDefinition<TPayload>,
|
|
payload: TPayload,
|
|
options: {
|
|
tenantId?: string | null;
|
|
dispatchKey?: string | null;
|
|
} = {}
|
|
): Promise<void> {
|
|
try {
|
|
await db.$transaction(async (tx) => {
|
|
await emitPluginHook(
|
|
tx,
|
|
def,
|
|
payload,
|
|
async (innerTx, args) => {
|
|
await publishOutboxEvent(innerTx as OutboxPublisherTransaction, {
|
|
tenantId: args.tenantId,
|
|
type: args.type,
|
|
payload: args.payload as unknown as Parameters<typeof publishOutboxEvent>[1]["payload"],
|
|
dispatchKey: args.dispatchKey,
|
|
});
|
|
},
|
|
options
|
|
);
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof PluginHookValidationError) {
|
|
pluginHookLogger.warn(
|
|
{
|
|
hookName: def.name,
|
|
tenantId: options.tenantId ?? null,
|
|
issues: error.issues,
|
|
},
|
|
"plugin.hook.payload_invalid"
|
|
);
|
|
return;
|
|
}
|
|
pluginHookLogger.error(
|
|
{
|
|
hookName: def.name,
|
|
tenantId: options.tenantId ?? null,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
},
|
|
"plugin.hook.emit_failed"
|
|
);
|
|
}
|
|
}
|