From 761a66237b66caf664c44bf6eae74b16efd0dc29 Mon Sep 17 00:00:00 2001 From: iinaa-eimrit Date: Mon, 17 Aug 2026 13:49:13 +0530 Subject: [PATCH] fix(effect): scheduled no-payload actions fail validation --- .../effect/src/internal/ActionDispatcher.ts | 16 +++++++++++- .../packages/effect/test/e2e.test.ts | 22 ++++++++++++++++ .../packages/effect/test/fixtures/actors.ts | 26 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts index eae860841c..c7d1f8fab5 100644 --- a/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts +++ b/rivetkit-typescript/packages/effect/src/internal/ActionDispatcher.ts @@ -85,8 +85,22 @@ export const make = < // Raw RivetKit clients call no-argument actions with an // absent first argument. The Effect JSON Void codec expects // null, so adapt only actions that declared no payload. + // Additionally, scheduled no-payload actions receive ScheduledFireInfo + // in the payload position, which must also decode as null. + const isScheduledFireInfo = (val: unknown): boolean => + typeof val === "object" && + val !== null && + "kind" in val && + "scheduledAt" in val && + "firedAt" in val; + + const isMisalignedSchedulePayload = + !action.hasPayload && + meta === undefined && + isScheduledFireInfo(payload); + const payloadForDecode = - !action.hasPayload && payload === undefined + !action.hasPayload && (payload === undefined || isMisalignedSchedulePayload) ? null : payload; const decodedPayload = yield* decodePayload( diff --git a/rivetkit-typescript/packages/effect/test/e2e.test.ts b/rivetkit-typescript/packages/effect/test/e2e.test.ts index 3bb7dced1a..ae31b42347 100644 --- a/rivetkit-typescript/packages/effect/test/e2e.test.ts +++ b/rivetkit-typescript/packages/effect/test/e2e.test.ts @@ -28,6 +28,8 @@ import { Unregistered, WakeDecodeFail, WakeDecodeFailLive, + ScheduleReproActor, + ScheduleReproActorLive, } from "./fixtures/actors"; import { TestTracer } from "./fixtures/tracer"; import { prepareNamespace, waitForEnvoy } from "./shared-engine"; @@ -81,6 +83,7 @@ const TestLayer = ReadyForEnvoy.pipe( WakeDecodeFailLive, BuildSetRejectedLive, TransformedStateActorLive, + ScheduleReproActorLive, ), ), Layer.provideMerge(Flags.layer), @@ -100,6 +103,25 @@ const TestLayer = ReadyForEnvoy.pipe( ); layer(TestLayer)("end-to-end", (it) => { + it.effect("scheduled no-payload actions execute successfully", () => + Effect.gen(function* () { + const flags = yield* Flags; + const client = yield* ScheduleReproActor.client; + const actor = client.getOrCreate("t-schedule"); + + yield* actor.ScheduleReproSchedule(); + + const fired = yield* Effect.sync(() => flags.get("schedule_repro_fired")).pipe( + Effect.repeat({ + until: (v) => v === true, + schedule: Schedule.spaced("100 millis"), + }), + TestClock.withLive, + ); + assert.strictEqual(fired, true); + }), + ); + it.effect("round-trips an action with payload and success", () => Effect.gen(function* () { const counter = (yield* Counter.client).getOrCreate("t-roundtrip"); diff --git a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts index 06da8cf362..a53deb52b7 100644 --- a/rivetkit-typescript/packages/effect/test/fixtures/actors.ts +++ b/rivetkit-typescript/packages/effect/test/fixtures/actors.ts @@ -733,3 +733,29 @@ export const BuildSetRejectedLive = BuildSetRejected.toLayer( }, }, ); + +export const ScheduleReproAction = Action.make("ScheduleReproAction"); +export const ScheduleReproSchedule = Action.make("ScheduleReproSchedule"); +export const ScheduleReproActor = Actor.make("ScheduleReproActor", { + actions: [ScheduleReproAction, ScheduleReproSchedule], +}); +export const ScheduleReproActorLive = ScheduleReproActor.toLayer( + ({ rawRivetkitContext }) => + Effect.gen(function* () { + const flags = yield* Flags; + return ScheduleReproActor.of({ + ScheduleReproAction: () => + Effect.sync(() => { + flags.set("schedule_repro_fired", true); + return null; + }), + ScheduleReproSchedule: () => + Effect.gen(function* () { + yield* Effect.promise(() => + rawRivetkitContext.schedule.after(100, "ScheduleReproAction"), + ); + return null; + }), + }); + }), +);