Skip to content

Commit d5b2a73

Browse files
feat(world-postgres): event-first start-hook admission
Adds the workflow_hook_claims table (single-owner token constraint; lifecycle state derived from hookId/expiresAt). run_created claims the token in the same transaction as the run and event inserts, createHook materializes the claim, disposal and run completion retain TTL-carrying claims with set-based statements, cancellation releases unmaterialized claims, and terminal-run cleanup GCs expired claim debris. Also lifts the postgres-lane skip from the start-hook e2e test.
1 parent 01a094a commit d5b2a73

8 files changed

Lines changed: 1055 additions & 182 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/world-postgres": minor
3+
---
4+
5+
Support experimental start-hook admission (event-first): a new `workflow_hook_claims` table backs single-owner hook tokens; `run_created` claims the token in the same transaction as run and event creation, claims are retained for their TTL after hook disposal or run completion, and cancellation releases claims the workflow never materialized.

packages/core/e2e/e2e.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1863,12 +1863,7 @@ describe('e2e', () => {
18631863
}
18641864
);
18651865

1866-
// Postgres world support lands in the next PR of this stack; drop the
1867-
// WORKFLOW_TARGET_WORLD clause there.
1868-
test.skipIf(
1869-
!!process.env.WORKFLOW_VERCEL_ENV ||
1870-
process.env.WORKFLOW_TARGET_WORLD === '@workflow/world-postgres'
1871-
)(
1866+
test.skipIf(!!process.env.WORKFLOW_VERCEL_ENV)(
18721867
'experimentalStartHook rejects duplicate starts before hook materialization',
18731868
{ timeout: 60_000 },
18741869
async () => {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE IF NOT EXISTS "workflow"."workflow_hook_claims" (
2+
"token" varchar PRIMARY KEY NOT NULL,
3+
"run_id" varchar NOT NULL,
4+
"hook_id" varchar,
5+
"ttl_seconds" integer NOT NULL,
6+
"expires_at" timestamp,
7+
"created_at" timestamp DEFAULT now() NOT NULL
8+
);
9+
--> statement-breakpoint
10+
CREATE INDEX IF NOT EXISTS "workflow_hook_claims_run_id_index" ON "workflow"."workflow_hook_claims" USING btree ("run_id");
11+
--> statement-breakpoint
12+
CREATE INDEX IF NOT EXISTS "workflow_hook_claims_expires_at_index" ON "workflow"."workflow_hook_claims" USING btree ("expires_at");

packages/world-postgres/src/drizzle/migrations/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@
113113
"when": 1782691200000,
114114
"tag": "0015_move_enums_to_workflow_schema",
115115
"breakpoints": true
116+
},
117+
{
118+
"idx": 16,
119+
"version": "7",
120+
"when": 1782777600000,
121+
"tag": "0016_add_hook_claims",
122+
"breakpoints": true
116123
}
117124
]
118125
}

packages/world-postgres/src/drizzle/schema.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,33 @@ export const hooks = schema.table(
218218
(tb) => [index().on(tb.runId), index().on(tb.token)]
219219
);
220220

221+
/**
222+
* Hook-token claims: the single-owner constraint behind hook tokens.
223+
*
224+
* A claim's lifecycle state is derived, not stored:
225+
* - unmaterialized start claim: `hookId IS NULL` (reserved by `start()`,
226+
* no hook entity yet)
227+
* - materialized: `hookId` set, `expiresAt IS NULL` (live hook entity)
228+
* - retained: `expiresAt` set (hook disposed / run finished; the token
229+
* stays fenced until `expiresAt`)
230+
*
231+
* `ttlSeconds <= 0` marks a plain `createHook` token guard with no
232+
* retention window — it is released when the hook is disposed or the run
233+
* reaches a terminal state.
234+
*/
235+
export const hookClaims = schema.table(
236+
'workflow_hook_claims',
237+
{
238+
token: varchar('token').primaryKey(),
239+
runId: varchar('run_id').notNull(),
240+
hookId: varchar('hook_id'),
241+
ttlSeconds: integer('ttl_seconds').notNull(),
242+
expiresAt: timestamp('expires_at'),
243+
createdAt: timestamp('created_at').defaultNow().notNull(),
244+
},
245+
(tb) => [index().on(tb.runId), index().on(tb.expiresAt)]
246+
);
247+
221248
export const waits = schema.table(
222249
'workflow_waits',
223250
{

packages/world-postgres/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export function createWorld(
5858

5959
return {
6060
specVersion: SPEC_VERSION_CURRENT,
61+
experimentalStartHookAdmission: { mode: 'event-first' },
6162
...storage,
6263
...streamer,
6364
...queue,

0 commit comments

Comments
 (0)