Skip to content

Commit 91c5b67

Browse files
committed
Align durable handler contexts
1 parent f21bb38 commit 91c5b67

7 files changed

Lines changed: 49 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Align effect and commit-action context with their durable source message and
6+
activation fence, using immutable TypeScript contracts.
57
- Bound runtime shutdown with a configurable deadline and report components
68
that do not cooperate with cancellation or stop before it expires.
79
- Separate generated request identity from caller-supplied idempotency keys and

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,10 @@ runtime.registerEffect("chargePayment", async ({ paymentId }, context) => {
405405
})
406406
```
407407

408+
Effect context also exposes `attempt`, `sourceMessageId`, `actorType`, and
409+
`actorId`. The effect `id` is stable across retries and remains the external
410+
idempotency key.
411+
408412
Success callbacks receive `{ effectId, result }`. Failure callbacks receive
409413
`{ effectId, error }`.
410414

@@ -419,6 +423,10 @@ runtime.registerCommitAction("completeAttempt", async ({ attemptId }, context) =
419423
})
420424
```
421425

426+
Commit-action context includes the source message and request IDs, actor
427+
identity, mailbox sequence, activation generation, and the active transaction
428+
connection.
429+
422430
Do not perform network I/O in a commit action. Use an effect when work cannot
423431
share the Solid Objects database transaction.
424432

docs/parity.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ Reference: Ruby `solid_objects` 0.12.0 at commit `a01b6f5`.
2828
| Bounded activation passes and hot-actor fairness | Native | Configurable turn-count and elapsed-time budgets bound each pass, then move only that actor's already-due memberships behind actors already waiting. |
2929
| Bounded claim candidate scan | Native | A configurable ordered scan continues to another ready actor when a worker loses the first candidate's lease race. |
3030
| Idle activation cache | Native | Long-running workers retain hydrated actors under renewable fenced leases, restore public state after failed turns, and release on timeout, fairness yield, lease loss, or shutdown. |
31-
| Transactional effects and outcome operations | Native | At-least-once effect handlers with stable IDs and success/failure actor operations. |
31+
| Transactional effects and outcome operations | Native | At-least-once handlers receive immutable stable effect, attempt, source-message, and actor identity; success and failure return through actor operations. |
3232
| Actor-to-actor delivery | Native | `sendTo(reference).operation()` stages delivery in the source actor commit. |
3333
| One-shot and recurring reminders | Native | Scheduling, replacement events, catch-up policy, stale-claim recovery, pausing, authorized inspection, and idempotent resume are implemented. |
34-
| Same-database commit actions | Native | Registered actions receive the fenced transaction connection. |
34+
| Same-database commit actions | Native | Registered actions receive source-message identity, mailbox sequence, activation generation, and the fenced transaction connection. |
3535
| Ambient transaction rejection | Native | Committed calls and message waits fail before blocking when the current async context already owns a transaction on the Solid Objects adapter. |
3636
| Direct application-write isolation during actor code | Partial | `guardApplicationDatabase()` fails closed for operations, projections, and migrations, while registered commit actions remain writable. Unwrapped ORM pools and third-party clients cannot be intercepted. |
3737
| Committed snapshots | Native | `snapshot()` returns authorized committed state; realtime replay reads the explicit observable projection with instance ID and revision without creating mailbox history. |

src/runtime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,7 @@ export class SolidObjectsRuntime {
11241124
messageId: turn.message.id,
11251125
requestId: turn.message.request_id,
11261126
sequence: BigInt(turn.message.sequence),
1127+
activationGeneration: turn.activationGeneration,
11271128
connection,
11281129
})
11291130
this.emitInstrumentation("commit_action.completed", attributes)
@@ -1282,7 +1283,7 @@ export class SolidObjectsRuntime {
12821283
attempt: Number(effect.attempt_count),
12831284
actorType: effect.actor_type,
12841285
actorId: effect.actor_id,
1285-
messageId: effect.message_id,
1286+
sourceMessageId: effect.message_id,
12861287
})) ?? null,
12871288
{ maxBytes: this.settings.maxResultBytes },
12881289
)

src/types.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,20 +48,21 @@ export interface MessageContext {
4848
}
4949

5050
export interface EffectContext {
51-
id: string
52-
attempt: number
53-
actorType: string
54-
actorId: string
55-
messageId: string
51+
readonly id: string
52+
readonly attempt: number
53+
readonly sourceMessageId: string
54+
readonly actorType: string
55+
readonly actorId: string
5656
}
5757

5858
export interface CommitActionContext {
59-
actorType: string
60-
actorId: string
61-
messageId: string
62-
requestId: string
63-
sequence: bigint
64-
connection: DatabaseConnection
59+
readonly actorType: string
60+
readonly actorId: string
61+
readonly messageId: string
62+
readonly requestId: string
63+
readonly sequence: bigint
64+
readonly activationGeneration: bigint
65+
readonly connection: DatabaseConnection
6566
}
6667

6768
export type MessageStatus = "ready" | "claimed" | "completed" | "rejected" | "dead" | "unknown"

test/outboxes.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,19 @@ afterEach(async () => {
8080
describe("durable effects", () => {
8181
it("runs an effect and delivers its success message", async () => {
8282
runtime = configuredRuntime()
83-
runtime.registerEffect("charge_payment", ({ paymentId }, context) => ({
84-
receipt: `${paymentId}:${context.id}`,
85-
}))
83+
let effectContext:
84+
| {
85+
id: string
86+
attempt: number
87+
sourceMessageId: string
88+
actorType: string
89+
actorId: string
90+
}
91+
| undefined
92+
runtime.registerEffect("charge_payment", ({ paymentId }, context) => {
93+
effectContext = context
94+
return { receipt: `${paymentId}:${context.id}` }
95+
})
8696
await runtime.install()
8797
const checkout = Checkout.ref("order-1")
8898

@@ -92,6 +102,12 @@ describe("durable effects", () => {
92102

93103
expect(await checkout.status).toBe("paid")
94104
expect(await checkout.effectResult).toMatch(/^payment-1:/)
105+
expect(effectContext).toMatchObject({
106+
attempt: 1,
107+
sourceMessageId: expect.any(String),
108+
actorType: Checkout.actorType,
109+
actorId: "order-1",
110+
})
95111
})
96112

97113
it("marks unknown effects dead and delivers the failure message", async () => {
@@ -140,7 +156,9 @@ describe("commit actions", () => {
140156
await runtime.settings.database.connection((connection) =>
141157
connection.run("CREATE TABLE application_records(id TEXT PRIMARY KEY) STRICT"),
142158
)
159+
let activationGeneration: bigint | undefined
143160
runtime.registerCommitAction("write_record", async ({ recordId }, context) => {
161+
activationGeneration = context.activationGeneration
144162
await context.connection.run("INSERT INTO application_records(id) VALUES (?)", [recordId])
145163
})
146164
const writer = DatabaseWriter.ref("writer")
@@ -154,6 +172,7 @@ describe("commit actions", () => {
154172
}>("SELECT id FROM application_records"),
155173
)
156174
expect(record?.id).toBe("record-1")
175+
expect(activationGeneration).toBeGreaterThan(0n)
157176
})
158177

159178
it("rolls actor state back when a commit action fails", async () => {

test/test-helper.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe("public test helper", () => {
5353
},
5454
})
5555
runtime.registerEffect("recordHelperEffect", async (_arguments, context) => {
56-
effects.push(context.messageId)
56+
effects.push(context.sourceMessageId)
5757
})
5858
await runtime.install()
5959
const message = await HelperActor.ref("workflow").send.startWork()

0 commit comments

Comments
 (0)