effect-inngest 0.1.2 → 0.2.0-beta.0

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.
Files changed (47) hide show
  1. package/dist/Client.d.ts +55 -24
  2. package/dist/Client.js +84 -25
  3. package/dist/Events.d.ts +36 -61
  4. package/dist/Events.js +5 -13
  5. package/dist/Function.d.ts +35 -15
  6. package/dist/Function.js +14 -8
  7. package/dist/Group.d.ts +5 -4
  8. package/dist/Group.js +26 -24
  9. package/dist/HttpApi.d.ts +49 -54
  10. package/dist/HttpApi.js +32 -17
  11. package/dist/_virtual/_rolldown/runtime.js +13 -0
  12. package/dist/index.js +1 -2
  13. package/dist/internal/checkpoint.d.ts +32 -0
  14. package/dist/internal/checkpoint.js +112 -0
  15. package/dist/internal/constants.js +1 -2
  16. package/dist/internal/driver.d.ts +1 -5
  17. package/dist/internal/driver.js +164 -52
  18. package/dist/internal/errors.d.ts +20 -27
  19. package/dist/internal/errors.js +29 -15
  20. package/dist/internal/handler.d.ts +4 -13
  21. package/dist/internal/handler.js +70 -43
  22. package/dist/internal/helpers.js +12 -9
  23. package/dist/internal/interrupts.d.ts +1 -2
  24. package/dist/internal/interrupts.js +1 -3
  25. package/dist/internal/memo.js +22 -20
  26. package/dist/internal/protocol.d.ts +28 -1
  27. package/dist/internal/protocol.js +84 -52
  28. package/dist/internal/signature.d.ts +5 -9
  29. package/dist/internal/signature.js +34 -18
  30. package/dist/internal/step.d.ts +5 -11
  31. package/dist/internal/step.js +48 -32
  32. package/package.json +26 -21
  33. package/src/Client.ts +244 -68
  34. package/src/Events.ts +3 -3
  35. package/src/Function.ts +52 -15
  36. package/src/Group.ts +39 -26
  37. package/src/HttpApi.ts +38 -27
  38. package/src/internal/checkpoint.ts +218 -0
  39. package/src/internal/driver.ts +241 -93
  40. package/src/internal/errors.ts +21 -14
  41. package/src/internal/handler.ts +185 -142
  42. package/src/internal/helpers.ts +9 -9
  43. package/src/internal/memo.ts +48 -33
  44. package/src/internal/protocol.ts +89 -45
  45. package/src/internal/signature.ts +76 -58
  46. package/src/internal/step.ts +83 -32
  47. package/dist/_virtual/rolldown_runtime.js +0 -18
@@ -2,21 +2,25 @@
2
2
  * Wire protocol schemas and opcode factories for Inngest communication.
3
3
  * @internal
4
4
  */
5
- import { Predicate, Struct } from "effect";
5
+ import { Effect, Predicate, SchemaTransformation, Struct } from "effect";
6
6
  import * as Schema from "effect/Schema";
7
7
 
8
8
  const stripTopLevelTag = (value: unknown): unknown => {
9
- if (Predicate.isRecord(value)) {
10
- return Struct.omit(value as Record<string, unknown>, "_tag");
9
+ if (Predicate.isObject(value)) {
10
+ return Struct.omit(value as Record<string, unknown>, ["_tag"]);
11
11
  }
12
12
  return value;
13
13
  };
14
14
 
15
- const WireUnknown = Schema.transform(Schema.Unknown, Schema.Unknown, {
16
- strict: true,
17
- decode: (value) => value,
18
- encode: (value) => stripTopLevelTag(value),
19
- });
15
+ const WireUnknown = Schema.Unknown.pipe(
16
+ Schema.decodeTo(
17
+ Schema.Unknown,
18
+ SchemaTransformation.transform({
19
+ decode: (value) => value,
20
+ encode: (value) => stripTopLevelTag(value),
21
+ }),
22
+ ),
23
+ );
20
24
 
21
25
  export const Opcode = {
22
26
  None: "None",
@@ -48,9 +52,7 @@ export class UserError extends Schema.Class<UserError>("UserError")({
48
52
  }) {}
49
53
 
50
54
  export const StepResult = Schema.NullOr(
51
- Schema.Record({ key: Schema.String, value: Schema.Unknown }).pipe(
52
- Schema.annotations({ identifier: "StepResultObject" }),
53
- ),
55
+ Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.annotate({ identifier: "StepResultObject" })),
54
56
  );
55
57
 
56
58
  export class FunctionStack extends Schema.Class<FunctionStack>("FunctionStack")({
@@ -61,36 +63,37 @@ export class FunctionStack extends Schema.Class<FunctionStack>("FunctionStack")(
61
63
  export class InngestEvent extends Schema.Class<InngestEvent>("InngestEvent")({
62
64
  id: Schema.optional(Schema.String),
63
65
  name: Schema.String,
64
- data: Schema.optionalWith(Schema.Record({ key: Schema.String, value: Schema.Unknown }), {
65
- default: () => ({}),
66
- nullable: true,
67
- }),
66
+ data: Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)).pipe(
67
+ Schema.withDecodingDefaultType(Effect.succeed({})),
68
+ ),
68
69
 
69
70
  ts: Schema.optional(Schema.Number),
70
- user: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
71
+ user: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
71
72
  v: Schema.optional(Schema.String),
72
73
  }) {}
73
74
 
74
75
  export class SDKRequestContext extends Schema.Class<SDKRequestContext>("SDKRequestContext")({
75
76
  fn_id: Schema.String,
76
77
  run_id: Schema.String,
77
- env: Schema.optionalWith(Schema.String, { default: () => "dev" }),
78
- step_id: Schema.optionalWith(Schema.String, { default: () => "step" }),
79
- attempt: Schema.optionalWith(Schema.Number, { default: () => 0 }),
80
- max_attempts: Schema.optionalWith(Schema.Number, { default: () => 4 }),
81
- stack: Schema.optionalWith(FunctionStack, { default: () => FunctionStack.make({ stack: [], current: 0 }) }),
82
- qi_id: Schema.optionalWith(Schema.String, { default: () => "" }),
83
- disable_immediate_execution: Schema.optionalWith(Schema.Boolean, { default: () => false }),
84
- use_api: Schema.optionalWith(Schema.Boolean, { default: () => false }),
78
+ env: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("dev"))),
79
+ step_id: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("step"))),
80
+ attempt: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(0))),
81
+ max_attempts: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(4))),
82
+ stack: FunctionStack.pipe(
83
+ Schema.withDecodingDefaultType(Effect.succeed(FunctionStack.make({ stack: [], current: 0 }))),
84
+ ),
85
+ qi_id: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
86
+ disable_immediate_execution: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
87
+ use_api: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
85
88
  }) {}
86
89
 
87
90
  export class SDKRequestBody extends Schema.Class<SDKRequestBody>("SDKRequestBody")({
88
91
  event: InngestEvent,
89
92
  events: Schema.Array(InngestEvent),
90
- steps: Schema.optionalWith(Schema.Record({ key: Schema.String, value: StepResult }), { default: () => ({}) }),
93
+ steps: Schema.Record(Schema.String, StepResult).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
91
94
  ctx: SDKRequestContext,
92
- version: Schema.optionalWith(Schema.Number, { default: () => 1 }),
93
- use_api: Schema.optionalWith(Schema.Boolean, { default: () => false }),
95
+ version: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(1))),
96
+ use_api: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
94
97
  }) {}
95
98
 
96
99
  export const Headers = {
@@ -108,7 +111,7 @@ export const Headers = {
108
111
  } as const;
109
112
 
110
113
  export class GeneratorOpcode extends Schema.Class<GeneratorOpcode>("GeneratorOpcode")({
111
- op: Schema.Literal(
114
+ op: Schema.Literals([
112
115
  Opcode.None,
113
116
  Opcode.Step,
114
117
  Opcode.StepRun,
@@ -124,10 +127,10 @@ export class GeneratorOpcode extends Schema.Class<GeneratorOpcode>("GeneratorOpc
124
127
  Opcode.StepFailed,
125
128
  Opcode.SyncRunComplete,
126
129
  Opcode.DiscoveryRequest,
127
- ),
130
+ ]),
128
131
  id: Schema.String,
129
132
  name: Schema.String,
130
- mode: Schema.optional(Schema.Literal("sync", "async")),
133
+ mode: Schema.optional(Schema.Literals(["sync", "async"])),
131
134
  opts: Schema.optional(WireUnknown),
132
135
  data: Schema.optional(WireUnknown),
133
136
  error: Schema.optional(UserError),
@@ -158,9 +161,17 @@ export const stepError = (info: StepInfo, error: UserError, noRetry?: boolean):
158
161
  };
159
162
 
160
163
  export const sleep = (info: StepInfo, duration: string): GeneratorOpcode =>
161
- // Official SDK puts duration/timestamp in `name` field, not opts.duration
162
- // mode: "async" is required per the Op type definition
163
- GeneratorOpcode.make({ op: Opcode.Sleep, id: info.hash, name: duration, displayName: info.name, mode: "async" });
164
+ // Spec §5.3.2 requires `opts.duration`. The Inngest executor also accepts
165
+ // `name: duration` (de facto inngest-js behavior); we emit both so both
166
+ // the spec-strict path and the official-SDK-lenient path see a value.
167
+ GeneratorOpcode.make({
168
+ op: Opcode.Sleep,
169
+ id: info.hash,
170
+ name: duration,
171
+ displayName: info.name,
172
+ mode: "async",
173
+ opts: { duration },
174
+ });
164
175
 
165
176
  export const waitForEvent = (info: StepInfo, opts: { event: string; timeout: string; if?: string }): GeneratorOpcode =>
166
177
  mkOpcode(info, Opcode.WaitForEvent, { mode: "async", opts });
@@ -170,27 +181,41 @@ export const invokeFunction = (
170
181
  opts: { function_id: string; payload: unknown; timeout: string },
171
182
  ): GeneratorOpcode => mkOpcode(info, Opcode.InvokeFunction, { mode: "async", opts, userland: { id: info.id } });
172
183
 
184
+ /**
185
+ * Terminal opcode emitted by the SDK in checkpoint mode when the function
186
+ * completes successfully. The executor uses this to correlate run completion
187
+ * (spec §10.4.1).
188
+ */
189
+ export const runComplete = (data: unknown): GeneratorOpcode =>
190
+ GeneratorOpcode.make({ op: Opcode.RunComplete, id: "step", name: "step", data });
191
+
192
+ /**
193
+ * Yield opcode emitted by the SDK in checkpoint mode when `maxRuntime` is
194
+ * exceeded. Tells the executor to schedule a new Call Request to continue
195
+ * execution (spec §10.4.1).
196
+ */
197
+ export const discoveryRequest = (): GeneratorOpcode =>
198
+ GeneratorOpcode.make({ op: Opcode.DiscoveryRequest, id: "step", name: "step" });
199
+
173
200
  const IntrospectionBase = Schema.Struct({
174
201
  function_count: Schema.Number,
175
202
  has_event_key: Schema.Boolean,
176
203
  has_signing_key: Schema.Boolean,
177
204
  has_signing_key_fallback: Schema.Boolean,
178
- mode: Schema.Literal("cloud", "dev"),
205
+ mode: Schema.Literals(["cloud", "dev"]),
179
206
  schema_version: Schema.Literal("2024-05-24"),
180
- extra: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
207
+ extra: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
181
208
  });
182
209
 
183
- export const IntrospectionUnauthenticated = Schema.extend(
184
- IntrospectionBase,
185
- Schema.Struct({
186
- authentication_succeeded: Schema.Union(Schema.Literal(false), Schema.Null),
187
- functions: Schema.optionalWith(Schema.Array(Schema.Unknown), { exact: true }),
210
+ export const IntrospectionUnauthenticated = IntrospectionBase.pipe(
211
+ Schema.fieldsAssign({
212
+ authentication_succeeded: Schema.Union([Schema.Literal(false), Schema.Null]),
213
+ functions: Schema.optionalKey(Schema.Array(Schema.Unknown)),
188
214
  }),
189
215
  );
190
216
 
191
- export const IntrospectionAuthenticated = Schema.extend(
192
- IntrospectionBase,
193
- Schema.Struct({
217
+ export const IntrospectionAuthenticated = IntrospectionBase.pipe(
218
+ Schema.fieldsAssign({
194
219
  authentication_succeeded: Schema.Literal(true),
195
220
  api_origin: Schema.String,
196
221
  app_id: Schema.String,
@@ -207,8 +232,27 @@ export const IntrospectionAuthenticated = Schema.extend(
207
232
  }),
208
233
  );
209
234
 
210
- export const IntrospectionResponse = Schema.Union(IntrospectionAuthenticated, IntrospectionUnauthenticated);
235
+ export const IntrospectionResponse = Schema.Union([IntrospectionAuthenticated, IntrospectionUnauthenticated]);
211
236
 
237
+ /**
238
+ * SDK → executor response shape for PUT sync requests per spec §4.3.1.
239
+ */
212
240
  export const RegisterResponse = Schema.Struct({
213
- message: Schema.optional(Schema.String),
241
+ message: Schema.String,
242
+ modified: Schema.Boolean,
214
243
  });
244
+
245
+ /**
246
+ * Executor → SDK response shape from `POST /fn/register` per spec §4.3.4.
247
+ * On success the body has `{ ok: true, modified?: boolean }`; on failure
248
+ * the body may carry an `error` string.
249
+ */
250
+ export const RegisterServerResponse = Schema.Union([
251
+ Schema.Struct({
252
+ ok: Schema.Literal(true),
253
+ modified: Schema.optional(Schema.Boolean),
254
+ }),
255
+ Schema.Struct({
256
+ error: Schema.optional(Schema.String),
257
+ }),
258
+ ]);
@@ -8,13 +8,14 @@ import * as DateTime from "effect/DateTime";
8
8
  import * as Effect from "effect/Effect";
9
9
  import * as Layer from "effect/Layer";
10
10
  import * as Schema from "effect/Schema";
11
+ import * as SchemaTransformation from "effect/SchemaTransformation";
11
12
  import * as Protocol from "./protocol.js";
12
13
 
13
14
  /**
14
15
  * @internal
15
16
  */
16
- export class SignatureError extends Schema.TaggedError<SignatureError>()("SignatureError", {
17
- reason: Schema.Literal("missing_header", "invalid_format", "expired", "invalid_signature", "missing_signing_key"),
17
+ export class SignatureError extends Schema.TaggedErrorClass<SignatureError>()("SignatureError", {
18
+ reason: Schema.Literals(["missing_header", "invalid_format", "expired", "invalid_signature", "missing_signing_key"]),
18
19
  message: Schema.String,
19
20
  }) {}
20
21
 
@@ -40,20 +41,23 @@ export interface SignatureService {
40
41
  /**
41
42
  * @internal
42
43
  */
43
- export class Signature extends Context.Tag("effect-inngest/Signature")<Signature, SignatureService>() {}
44
+ export class Signature extends Context.Service<Signature, SignatureService>()("effect-inngest/Signature") {}
44
45
 
45
46
  // ─────────────────────────────────────────────────────────────
46
47
  // Schemas (internal)
47
48
  // ─────────────────────────────────────────────────────────────
48
49
 
49
- const TimestampSeconds = Schema.NumberFromString.pipe(Schema.int(), Schema.positive());
50
+ const TimestampSeconds = Schema.NumberFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThan(0)));
50
51
  // Case-insensitive hex, normalized to lowercase in parseSignatureHeader
51
52
  const SignatureHex = Schema.String.pipe(
52
- Schema.pattern(/^[a-fA-F0-9]{64}$/),
53
- Schema.transform(Schema.String, {
54
- decode: (s) => s.toLowerCase(),
55
- encode: (s) => s,
56
- }),
53
+ Schema.check(Schema.isPattern(/^[a-fA-F0-9]{64}$/)),
54
+ Schema.decodeTo(
55
+ Schema.String,
56
+ SchemaTransformation.transform({
57
+ decode: (s) => s.toLowerCase(),
58
+ encode: (s) => s,
59
+ }),
60
+ ),
57
61
  );
58
62
  const SignatureParams = Schema.Struct({ t: TimestampSeconds, s: SignatureHex });
59
63
 
@@ -66,7 +70,7 @@ const SIGNATURE_VALIDITY_WINDOW_MS = 5 * 60 * 1000;
66
70
  const parseSignatureHeader = (header: string) => {
67
71
  const params = new URLSearchParams(header);
68
72
  const raw = { t: params.get("t") ?? "", s: params.get("s") ?? "" };
69
- return Schema.decodeUnknown(SignatureParams)(raw).pipe(
73
+ return Schema.decodeUnknownEffect(SignatureParams)(raw).pipe(
70
74
  Effect.mapError(
71
75
  () =>
72
76
  new SignatureError({
@@ -82,6 +86,18 @@ const extractKeyBytes = (signingKey: string): Buffer => {
82
86
  return Buffer.from(keyWithoutPrefix, "hex");
83
87
  };
84
88
 
89
+ /**
90
+ * SHA-256 hex of the signing key with the `signkey-{prefix}-` prefix stripped.
91
+ * Used as the Bearer token for outbound requests to the Inngest API per spec
92
+ * §4.1.1.
93
+ *
94
+ * @internal
95
+ */
96
+ export const hashSigningKey = (signingKey: string): string => {
97
+ const keyWithoutPrefix = signingKey.replace(/^signkey-\w+-/, "");
98
+ return Crypto.createHash("sha256").update(keyWithoutPrefix).digest("hex");
99
+ };
100
+
85
101
  const computeSignature = (keyBytes: Buffer, body: Uint8Array, timestamp: string): string =>
86
102
  Crypto.createHmac("sha256", keyBytes).update(body).update(timestamp).digest("hex");
87
103
 
@@ -107,52 +123,54 @@ const checkSignature = (signature: string, body: Uint8Array, timestamp: string,
107
123
  /**
108
124
  * @internal
109
125
  */
110
- export const SignatureLive: Layer.Layer<Signature> = Layer.effect(
111
- Signature,
112
- Effect.succeed({
113
- verify: ({ body, signatureHeader, signingKey, signingKeyFallback, isDev }) =>
114
- Effect.gen(function* () {
115
- if (isDev) return true;
116
- if (!signingKey) {
117
- return yield* new SignatureError({
118
- reason: "missing_signing_key",
119
- message: "No signing key configured for production mode",
120
- });
121
- }
122
-
123
- if (!signatureHeader) {
124
- return yield* new SignatureError({
125
- reason: "missing_header",
126
- message: `Missing ${Protocol.Headers.Signature} header`,
127
- });
128
- }
129
-
130
- const { t: timestampSeconds, s: signature } = yield* parseSignatureHeader(signatureHeader);
131
- const timestampMs = timestampSeconds * 1000;
132
- const now = yield* DateTime.now;
133
-
134
- if (Math.abs(now.epochMillis - timestampMs) > SIGNATURE_VALIDITY_WINDOW_MS) {
135
- return yield* new SignatureError({
136
- reason: "expired",
137
- message: `Signature expired: timestamp ${timestampSeconds} is outside the validity window`,
138
- });
139
- }
140
-
141
- const timestamp = String(timestampSeconds);
142
- const keys = [signingKey, signingKeyFallback].filter(Boolean) as string[];
143
- const valid = keys.some((key) => checkSignature(signature, body, timestamp, key));
144
-
145
- if (valid) return true;
146
-
147
- return yield* new SignatureError({ reason: "invalid_signature", message: "Invalid signature" });
148
- }),
149
-
150
- sign: (body, signingKey) =>
151
- DateTime.now.pipe(
152
- Effect.map((now) => {
153
- const ts = Math.floor(now.epochMillis / 1000);
154
- return `t=${ts}&s=${computeSignature(extractKeyBytes(signingKey), body, String(ts))}`;
155
- }),
156
- ),
126
+ export const SignatureLive: Layer.Layer<Signature> = Layer.succeed(Signature, {
127
+ verify: Effect.fn("Signature.verify")(function* ({
128
+ body,
129
+ signatureHeader,
130
+ signingKey,
131
+ signingKeyFallback,
132
+ isDev,
133
+ }: VerifyOptions) {
134
+ if (isDev) return true;
135
+ if (!signingKey) {
136
+ return yield* new SignatureError({
137
+ reason: "missing_signing_key",
138
+ message: "No signing key configured for production mode",
139
+ });
140
+ }
141
+
142
+ if (!signatureHeader) {
143
+ return yield* new SignatureError({
144
+ reason: "missing_header",
145
+ message: `Missing ${Protocol.Headers.Signature} header`,
146
+ });
147
+ }
148
+
149
+ const { t: timestampSeconds, s: signature } = yield* parseSignatureHeader(signatureHeader);
150
+ const timestampMs = timestampSeconds * 1000;
151
+ const now = yield* DateTime.now;
152
+
153
+ if (Math.abs(now.epochMilliseconds - timestampMs) > SIGNATURE_VALIDITY_WINDOW_MS) {
154
+ return yield* new SignatureError({
155
+ reason: "expired",
156
+ message: `Signature expired: timestamp ${timestampSeconds} is outside the validity window`,
157
+ });
158
+ }
159
+
160
+ const timestamp = String(timestampSeconds);
161
+ const keys = [signingKey, signingKeyFallback].filter(Boolean) as string[];
162
+ const valid = keys.some((key) => checkSignature(signature, body, timestamp, key));
163
+
164
+ if (valid) return true;
165
+
166
+ return yield* new SignatureError({ reason: "invalid_signature", message: "Invalid signature" });
157
167
  }),
158
- );
168
+
169
+ sign: (body, signingKey) =>
170
+ DateTime.now.pipe(
171
+ Effect.map((now) => {
172
+ const ts = Math.floor(now.epochMilliseconds / 1000);
173
+ return `t=${ts}&s=${computeSignature(extractKeyBytes(signingKey), body, String(ts))}`;
174
+ }),
175
+ ),
176
+ });
@@ -3,7 +3,6 @@
3
3
  * @internal
4
4
  */
5
5
  import * as Arr from "effect/Array";
6
- import * as Context from "effect/Context";
7
6
  import * as Duration from "effect/Duration";
8
7
  import * as Effect from "effect/Effect";
9
8
  import * as HashMap from "effect/HashMap";
@@ -15,6 +14,7 @@ import * as Schema from "effect/Schema";
15
14
  import { pipe } from "effect/Function";
16
15
  import { InngestClient } from "../Client.js";
17
16
  import type { InngestFunction } from "../Function.js";
17
+ import type { CheckpointState } from "./checkpoint.js";
18
18
  import * as Protocol from "./protocol.js";
19
19
  import { StepError, SendEventError, isNonRetriableError, isRetryAfterError } from "./errors.js";
20
20
  import { timeStr, formatTimestamp } from "./helpers.js";
@@ -68,7 +68,7 @@ interface StepOptions {
68
68
  type StepOptionsOrId = string | StepOptions;
69
69
 
70
70
  interface WaitForEventOptions {
71
- readonly timeout: Duration.DurationInput;
71
+ readonly timeout: Duration.Input;
72
72
  readonly if?: string;
73
73
  }
74
74
 
@@ -76,22 +76,22 @@ interface InvokeOptionsBase<F extends InngestFunction.Any> {
76
76
  readonly function: F;
77
77
  readonly user?: Record<string, unknown>;
78
78
  readonly v?: string;
79
- readonly timeout?: Duration.DurationInput;
79
+ readonly timeout?: Duration.Input;
80
80
  }
81
81
 
82
82
  type InvokeOptions<F extends InngestFunction.Any> = [InngestFunction.EventType<F>] extends [never]
83
83
  ? InvokeOptionsBase<F>
84
84
  : InvokeOptionsBase<F> & { readonly data: InngestFunction.EventType<F> };
85
85
 
86
- type EventSchema = Schema.Schema.Any & { readonly _tag: string };
86
+ type EventSchema = Schema.Top & { readonly identifier: string };
87
87
  type TaggedEvent = { readonly _tag: string };
88
88
 
89
- const encodeTaggedEvent = (event: TaggedEvent): Effect.Effect<unknown, never> => {
89
+ const encodeTaggedEvent = (event: TaggedEvent): Effect.Effect<unknown, never, never> => {
90
90
  const ctor = event.constructor;
91
91
  if (Schema.isSchema(ctor)) {
92
- return Schema.encode(ctor as unknown as Schema.Schema.AnyNoContext)(event).pipe(
92
+ return Schema.encodeEffect(ctor as unknown as Schema.Top)(event).pipe(
93
93
  Effect.orElseSucceed(() => event),
94
- );
94
+ ) as Effect.Effect<unknown, never, never>;
95
95
  }
96
96
  return Effect.succeed(event);
97
97
  };
@@ -101,7 +101,7 @@ interface StepTools {
101
101
  id: StepOptionsOrId,
102
102
  effect: Effect.Effect<A, Err, R>,
103
103
  ) => Effect.Effect<A, StepError | Err, R>;
104
- readonly sleep: (id: StepOptionsOrId, duration: Duration.DurationInput) => Effect.Effect<void>;
104
+ readonly sleep: (id: StepOptionsOrId, duration: Duration.Input) => Effect.Effect<void>;
105
105
  readonly sleepUntil: (id: StepOptionsOrId, timestamp: Date | number | string) => Effect.Effect<void>;
106
106
  readonly waitForEvent: <E extends EventSchema>(
107
107
  id: StepOptionsOrId,
@@ -130,8 +130,6 @@ export interface HandlerContext<F extends InngestFunction.Any> {
130
130
  readonly run: RunContext;
131
131
  }
132
132
 
133
- export class Step extends Context.Tag("effect-inngest/Step")<Step, StepTools>() {}
134
-
135
133
  const normalizeOpts = (opts: StepOptionsOrId): { id: string; name: string } =>
136
134
  typeof opts === "string" ? { id: opts, name: opts } : { id: opts.id, name: opts.name ?? opts.id };
137
135
 
@@ -142,6 +140,7 @@ export const createStepTools = (
142
140
  request: Protocol.SDKRequestBody,
143
141
  appName: string,
144
142
  stepIdCounts: Ref.Ref<HashMap.HashMap<string, number>>,
143
+ checkpoint: Option.Option<CheckpointState> = Option.none(),
145
144
  ): StepTools => {
146
145
  const ctx = request.ctx;
147
146
  const steps = request.steps as Record<string, unknown>;
@@ -161,7 +160,15 @@ export const createStepTools = (
161
160
  const canExecute = (hash: string) => ctx.step_id === hash || ctx.step_id === "step";
162
161
  const isBlocked = (hash: string) => ctx.disable_immediate_execution && ctx.step_id !== hash;
163
162
 
164
- const sleep = (opts: StepOptionsOrId, duration: Duration.DurationInput): Effect.Effect<void, StepInterrupt> =>
163
+ // In checkpoint mode, async opcodes (sleep/wait/invoke) MUST flush the
164
+ // buffer before yielding so the executor sees buffered StepRuns prepended
165
+ // to the async opcode in the 206 response (spec §10.2 / §10.4.1 #6).
166
+ const flushIfCheckpoint = Option.match(checkpoint, {
167
+ onNone: () => Effect.void,
168
+ onSome: (state) => state.flush,
169
+ });
170
+
171
+ const sleep = (opts: StepOptionsOrId, duration: Duration.Input): Effect.Effect<void, StepInterrupt> =>
165
172
  Effect.flatMap(getInfo(opts), (info) =>
166
173
  pipe(
167
174
  getMemo(info.hash),
@@ -170,7 +177,13 @@ export const createStepTools = (
170
177
  Match.tag("MemoTimeout", () => Effect.void),
171
178
  Match.tag("MemoError", () => Effect.void),
172
179
  Match.tag("MemoInput", () => Effect.void),
173
- Match.tag("MemoNone", () => Effect.fail(sleepInterrupt({ info, duration: timeStr(duration) }))),
180
+ Match.tag("MemoNone", () =>
181
+ Effect.andThen(flushIfCheckpoint, Effect.fail(sleepInterrupt({ info, duration: timeStr(duration) }))).pipe(
182
+ Effect.withSpan(`inngest.step/sleep/${info.id}`, {
183
+ attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sleep" },
184
+ }),
185
+ ),
186
+ ),
174
187
  Match.exhaustive,
175
188
  ),
176
189
  );
@@ -184,7 +197,16 @@ export const createStepTools = (
184
197
  Match.tag("MemoTimeout", () => Effect.void),
185
198
  Match.tag("MemoError", () => Effect.void),
186
199
  Match.tag("MemoInput", () => Effect.void),
187
- Match.tag("MemoNone", () => Effect.fail(sleepInterrupt({ info, duration: formatTimestamp(timestamp) }))),
200
+ Match.tag("MemoNone", () =>
201
+ Effect.andThen(
202
+ flushIfCheckpoint,
203
+ Effect.fail(sleepInterrupt({ info, duration: formatTimestamp(timestamp) })),
204
+ ).pipe(
205
+ Effect.withSpan(`inngest.step/sleepUntil/${info.id}`, {
206
+ attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sleepUntil" },
207
+ }),
208
+ ),
209
+ ),
188
210
  Match.exhaustive,
189
211
  ),
190
212
  );
@@ -212,8 +234,16 @@ export const createStepTools = (
212
234
  Match.tag("MemoError", () => Effect.succeed(Option.none())),
213
235
  Match.tag("MemoInput", () => Effect.succeed(Option.none())),
214
236
  Match.tag("MemoNone", () =>
215
- Effect.fail(
216
- waitForEventInterrupt({ info, event: event._tag, timeout: timeStr(options.timeout), if: options.if }),
237
+ Effect.andThen(
238
+ flushIfCheckpoint,
239
+ Effect.fail(
240
+ waitForEventInterrupt({
241
+ info,
242
+ event: event.identifier,
243
+ timeout: timeStr(options.timeout),
244
+ if: options.if,
245
+ }),
246
+ ),
217
247
  ),
218
248
  ),
219
249
  Match.exhaustive,
@@ -240,17 +270,20 @@ export const createStepTools = (
240
270
  const rawData = Predicate.hasProperty(options, "data") ? (options.data as TaggedEvent) : undefined;
241
271
  const encodeData = rawData ? encodeTaggedEvent(rawData) : Effect.succeed(undefined);
242
272
  return Effect.flatMap(encodeData, (encodedData) =>
243
- Effect.fail(
244
- invokeInterrupt({
245
- info,
246
- functionId: `${appName}-${options.function._tag}`,
247
- payload: {
248
- data: encodedData,
249
- user: options.user ?? {},
250
- v: options.v ?? "1",
251
- },
252
- timeout: options.timeout ? timeStr(options.timeout) : "365d",
253
- }),
273
+ Effect.andThen(
274
+ flushIfCheckpoint,
275
+ Effect.fail(
276
+ invokeInterrupt({
277
+ info,
278
+ functionId: `${appName}-${options.function._tag}`,
279
+ payload: {
280
+ data: encodedData,
281
+ user: options.user ?? {},
282
+ v: options.v ?? "1",
283
+ },
284
+ timeout: options.timeout ? timeStr(options.timeout) : "365d",
285
+ }),
286
+ ),
254
287
  ),
255
288
  );
256
289
  }),
@@ -268,8 +301,13 @@ export const createStepTools = (
268
301
  Match.value,
269
302
  Match.tag("MemoData", ({ data }) => Effect.succeed(data as A)),
270
303
  Match.tag("MemoError", ({ error }) =>
304
+ // Spec §5.2.2: an uncaught memoized step error MUST be treated as
305
+ // non-retriable. If the user wanted to recover they would have
306
+ // caught the StepError at the call site; reaching the driver's
307
+ // top-level failure path means the error is propagating unhandled.
271
308
  stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Step failed", {
272
309
  noRetry: true,
310
+ cause: error,
273
311
  }),
274
312
  ),
275
313
  Match.tag("MemoTimeout", () => stepError(info.id, "Step timed out", { noRetry: true })),
@@ -287,15 +325,23 @@ export const createStepTools = (
287
325
  onFailure: (err) => {
288
326
  const noRetry = isNonRetriableError(err) ? true : undefined;
289
327
  const retryAfterMs = isRetryAfterError(err) ? Duration.toMillis(err.retryAfter) : undefined;
290
- return Effect.zipRight(
328
+ return Effect.andThen(
291
329
  Effect.annotateCurrentSpan(errorOtelAttributes(err)),
292
330
  Effect.fail(errorInterrupt({ info, error: err, noRetry, retryAfterMs })),
293
331
  );
294
332
  },
295
- onSuccess: (data) => Effect.fail(runInterrupt({ info, data })),
333
+ onSuccess: (data) =>
334
+ Option.match(checkpoint, {
335
+ // Async (non-checkpoint) mode: surface result via interrupt — driver
336
+ // returns 206 with a single StepRun and yields back to executor.
337
+ onNone: () => Effect.fail(runInterrupt({ info, data })),
338
+ // Checkpoint mode: buffer the StepRun (best-effort flush handled by
339
+ // bufferStep) and continue execution with the value (spec §10.4.1).
340
+ onSome: (state) => Effect.as(state.bufferStep(Protocol.stepRun(info, data)), data as A),
341
+ }),
296
342
  }),
297
- Effect.catchAllDefect((defect) =>
298
- Effect.zipRight(
343
+ Effect.catchDefect((defect) =>
344
+ Effect.andThen(
299
345
  Effect.annotateCurrentSpan(errorOtelAttributes(defect)),
300
346
  Effect.fail(errorInterrupt({ info, error: defect })),
301
347
  ),
@@ -331,12 +377,17 @@ export const createStepTools = (
331
377
  Effect.map(encodeTaggedEvent(e), (encoded) => ({ name: e._tag, data: encoded })),
332
378
  ),
333
379
  (eventPayloads) =>
334
- Effect.flatMap(InngestClient, (client) =>
380
+ InngestClient.use((client) =>
335
381
  client.sendEvent(eventPayloads).pipe(
336
382
  Effect.withSpan(`inngest.step/sendEvent/${info.id}`, {
337
383
  attributes: { [OtelAttributes.StepId]: info.id, [OtelAttributes.StepType]: "sendEvent" },
338
384
  }),
339
- Effect.flatMap((result) => Effect.fail(runInterrupt({ info, data: result }))),
385
+ Effect.flatMap((result) =>
386
+ Option.match(checkpoint, {
387
+ onNone: () => Effect.fail(runInterrupt({ info, data: result })),
388
+ onSome: (state) => Effect.as(state.bufferStep(Protocol.stepRun(info, result)), result),
389
+ }),
390
+ ),
340
391
  ),
341
392
  ),
342
393
  );
@@ -1,18 +0,0 @@
1
- //#region rolldown:runtime
2
- var __defProp = Object.defineProperty;
3
- var __exportAll = (all, symbols) => {
4
- let target = {};
5
- for (var name in all) {
6
- __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- }
11
- if (symbols) {
12
- __defProp(target, Symbol.toStringTag, { value: "Module" });
13
- }
14
- return target;
15
- };
16
-
17
- //#endregion
18
- export { __exportAll };