@porulle/core 0.35.0 → 0.35.1

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.
@@ -1,4 +1,8 @@
1
1
  export type HookHandler = (...args: never[]) => unknown;
2
+ /** Declare that this handler must run inside the writing transaction. Called by the manifest merge. */
3
+ export declare function markHookInTransaction(handler: unknown): void;
4
+ /** Whether a plugin asked for this handler to run in-transaction. Read by the kernel at boot. */
5
+ export declare function isHookMarkedInTransaction(handler: unknown): boolean;
2
6
  export declare class HookRegistry {
3
7
  private registry;
4
8
  private inTransactionHandlers;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/kernel/hooks/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;AAQxD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,qBAAqB,CAA8B;IAC3D,OAAO,CAAC,MAAM,CAAC,CAAiE;IAEhF,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI;IAKpE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKpD,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKjE,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKrD,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKlE,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO;IAIhD,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE;IAMxC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,GAAG,IAAI;IAIjF,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD,OAAO,CAAC,WAAW;CASpB"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/kernel/hooks/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;AAgBxD,uGAAuG;AACvG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5D;AAED,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEnE;AAQD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,qBAAqB,CAA8B;IAC3D,OAAO,CAAC,MAAM,CAAC,CAAiE;IAEhF,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI;IAKpE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKpD,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKjE,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKrD,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI;IAKlE,iBAAiB,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO;IAIhD,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,EAAE;IAMxC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;KAAE,GAAG,IAAI;IAIjF,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxD,OAAO,CAAC,WAAW;CASpB"}
@@ -1,3 +1,25 @@
1
+ /**
2
+ * Handlers a PLUGIN asked to run inside the writing transaction.
3
+ *
4
+ * Module-level rather than per-registry because a plugin's declaration and its registration are
5
+ * separated by the manifest merge: `manifest.hooks()` returns `{ key, handler, inTransaction }`
6
+ * records, which are flattened into `config.hooks` as a bare `key -> handler[]` map long before any
7
+ * HookRegistry exists. Marking the function itself is the only channel that survives that.
8
+ *
9
+ * Shipped in 0.35.1 because 0.35.0 made after-commit the default with no way for a plugin to opt
10
+ * out, which silently moved `loom_projection_pending` — a transactional outbox whose whole point is
11
+ * committing with the write it records — to after the commit.
12
+ */
13
+ const pluginInTransactionHandlers = new WeakSet();
14
+ /** Declare that this handler must run inside the writing transaction. Called by the manifest merge. */
15
+ export function markHookInTransaction(handler) {
16
+ if (typeof handler === "function")
17
+ pluginInTransactionHandlers.add(handler);
18
+ }
19
+ /** Whether a plugin asked for this handler to run in-transaction. Read by the kernel at boot. */
20
+ export function isHookMarkedInTransaction(handler) {
21
+ return typeof handler === "function" && pluginInTransactionHandlers.has(handler);
22
+ }
1
23
  export class HookRegistry {
2
24
  registry = new Map();
3
25
  inTransactionHandlers = new WeakSet();
@@ -28,6 +28,15 @@ export type PluginRouteRegistration = {
28
28
  export interface PluginHookRegistration {
29
29
  key: string;
30
30
  handler: (...args: unknown[]) => unknown;
31
+ /**
32
+ * Run this hook INSIDE the writing transaction instead of after it commits.
33
+ *
34
+ * The default is after-commit and is correct for anything with an external effect — a webhook, a
35
+ * search-index write, an email — none of which may announce a write that can still roll back.
36
+ * Set this only for an OUTBOX WRITER: a hook whose own write must commit or roll back together
37
+ * with the row it records. `loom_projection_pending` is the case this exists for.
38
+ */
39
+ inTransaction?: boolean;
31
40
  }
32
41
  export interface PluginContext {
33
42
  config: CommerceConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/kernel/plugin/manifest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/kernel/plugin/manifest.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { markHookInTransaction } from "../hooks/registry.js";
1
2
  import { resolveOrgIdForCommerce } from "../../auth/org.js";
2
3
  import { isRoutePermissionGuard, markRoutePermissionGuard } from "../../interfaces/rest/utils.js";
3
4
  import { createScopedDb } from "../database/scoped-db.js";
@@ -141,6 +142,10 @@ export function defineCommercePlugin(manifest) {
141
142
  ...(result.hooks ?? {}),
142
143
  };
143
144
  for (const reg of registrations) {
145
+ // The marker rides on the function because this map is a bare key -> handler[] and cannot
146
+ // carry it; the kernel reads it back when it registers these at boot.
147
+ if (reg.inTransaction)
148
+ markHookInTransaction(reg.handler);
144
149
  hookMap[reg.key] = [...(hookMap[reg.key] ?? []), reg.handler];
145
150
  }
146
151
  result = { ...result, hooks: hookMap };
@@ -1 +1 @@
1
- {"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../../src/runtime/kernel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAqBzD,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AAC/C,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA0I3D"}
1
+ {"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../../src/runtime/kernel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAqBzD,OAAO,EAGL,KAAK,MAAM,EACX,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;AAC/C,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EACL,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,qBAAqB,CAAC;AAE7B,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAiJ3D"}
@@ -1,4 +1,4 @@
1
- import { HookRegistry } from "../kernel/hooks/registry.js";
1
+ import { HookRegistry, isHookMarkedInTransaction } from "../kernel/hooks/registry.js";
2
2
  import { createDatabaseConnection } from "../kernel/database/adapter.js";
3
3
  import { topoSortModules } from "../kernel/module/index.js";
4
4
  import { WebhookDeliveryWorker } from "../modules/webhooks/worker.js";
@@ -115,7 +115,15 @@ export function createKernel(config) {
115
115
  };
116
116
  for (const [key, handlers] of Object.entries(config.hooks ?? {})) {
117
117
  for (const handler of handlers) {
118
- hooks.append(key, handler);
118
+ // A plugin that declared `inTransaction` gets appendInTransaction, so its hook runs inside
119
+ // the writing transaction rather than in the after-commit drain. Everything else defaults to
120
+ // after-commit, which is what 0.35.0 established.
121
+ if (isHookMarkedInTransaction(handler)) {
122
+ hooks.appendInTransaction(key, handler);
123
+ }
124
+ else {
125
+ hooks.append(key, handler);
126
+ }
119
127
  }
120
128
  }
121
129
  for (const model of config.analytics?.models ?? []) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/core",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,5 +1,29 @@
1
1
  export type HookHandler = (...args: never[]) => unknown;
2
2
 
3
+ /**
4
+ * Handlers a PLUGIN asked to run inside the writing transaction.
5
+ *
6
+ * Module-level rather than per-registry because a plugin's declaration and its registration are
7
+ * separated by the manifest merge: `manifest.hooks()` returns `{ key, handler, inTransaction }`
8
+ * records, which are flattened into `config.hooks` as a bare `key -> handler[]` map long before any
9
+ * HookRegistry exists. Marking the function itself is the only channel that survives that.
10
+ *
11
+ * Shipped in 0.35.1 because 0.35.0 made after-commit the default with no way for a plugin to opt
12
+ * out, which silently moved `loom_projection_pending` — a transactional outbox whose whole point is
13
+ * committing with the write it records — to after the commit.
14
+ */
15
+ const pluginInTransactionHandlers = new WeakSet<object>();
16
+
17
+ /** Declare that this handler must run inside the writing transaction. Called by the manifest merge. */
18
+ export function markHookInTransaction(handler: unknown): void {
19
+ if (typeof handler === "function") pluginInTransactionHandlers.add(handler as object);
20
+ }
21
+
22
+ /** Whether a plugin asked for this handler to run in-transaction. Read by the kernel at boot. */
23
+ export function isHookMarkedInTransaction(handler: unknown): boolean {
24
+ return typeof handler === "function" && pluginInTransactionHandlers.has(handler as object);
25
+ }
26
+
3
27
  type HookEntry = {
4
28
  prepended: HookHandler[];
5
29
  configured: HookHandler[];
@@ -1,4 +1,5 @@
1
1
  import type { Hono } from "hono";
2
+ import { markHookInTransaction } from "../hooks/registry.js";
2
3
  import type { OpenAPIHono, RouteConfig } from "@hono/zod-openapi";
3
4
  import type { PluginDb } from "../database/plugin-types.js";
4
5
  import type { DatabaseAdapter } from "../database/adapter.js";
@@ -43,6 +44,15 @@ export type PluginRouteRegistration =
43
44
  export interface PluginHookRegistration {
44
45
  key: string;
45
46
  handler: (...args: unknown[]) => unknown;
47
+ /**
48
+ * Run this hook INSIDE the writing transaction instead of after it commits.
49
+ *
50
+ * The default is after-commit and is correct for anything with an external effect — a webhook, a
51
+ * search-index write, an email — none of which may announce a write that can still roll back.
52
+ * Set this only for an OUTBOX WRITER: a hook whose own write must commit or roll back together
53
+ * with the row it records. `loom_projection_pending` is the case this exists for.
54
+ */
55
+ inTransaction?: boolean;
46
56
  }
47
57
 
48
58
  // ─── Plugin Context (available to routes at boot time) ───────
@@ -294,6 +304,9 @@ export function defineCommercePlugin(
294
304
  ...(result.hooks ?? {}),
295
305
  };
296
306
  for (const reg of registrations) {
307
+ // The marker rides on the function because this map is a bare key -> handler[] and cannot
308
+ // carry it; the kernel reads it back when it registers these at boot.
309
+ if (reg.inTransaction) markHookInTransaction(reg.handler);
297
310
  hookMap[reg.key] = [...(hookMap[reg.key] ?? []), reg.handler];
298
311
  }
299
312
  result = { ...result, hooks: hookMap };
@@ -1,5 +1,5 @@
1
1
  import type { CommerceConfig } from "../config/types.js";
2
- import { HookRegistry, type HookHandler } from "../kernel/hooks/registry.js";
2
+ import { HookRegistry, isHookMarkedInTransaction, type HookHandler } from "../kernel/hooks/registry.js";
3
3
  import { createDatabaseConnection } from "../kernel/database/adapter.js";
4
4
  import type { DrizzleDatabase } from "../kernel/database/drizzle-db.js";
5
5
  import type { AppModule } from "../kernel/module/index.js";
@@ -162,7 +162,14 @@ export function createKernel(config: CommerceConfig): Kernel {
162
162
 
163
163
  for (const [key, handlers] of Object.entries(config.hooks ?? {})) {
164
164
  for (const handler of handlers) {
165
- hooks.append(key, handler as HookHandler);
165
+ // A plugin that declared `inTransaction` gets appendInTransaction, so its hook runs inside
166
+ // the writing transaction rather than in the after-commit drain. Everything else defaults to
167
+ // after-commit, which is what 0.35.0 established.
168
+ if (isHookMarkedInTransaction(handler)) {
169
+ hooks.appendInTransaction(key, handler as HookHandler);
170
+ } else {
171
+ hooks.append(key, handler as HookHandler);
172
+ }
166
173
  }
167
174
  }
168
175