Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/plugins/services/.server/pluginHookPublisher.ts
T

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"
);
}
}