effect-inngest 0.3.0-beta.3 → 0.3.0-beta.4

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.
package/dist/Group.d.ts CHANGED
@@ -127,7 +127,7 @@ declare const make: <Fns extends ReadonlyArray<InngestFunction.Any>>(...fns: Fns
127
127
  * )
128
128
  * ```
129
129
  */
130
- declare const toHttpApp: (group: InngestGroup.Any) => Effect.Effect<_$effect_unstable_http_HttpServerResponse0.HttpServerResponse, never, InngestClient | HttpClient.HttpClient | _$effect_unstable_http_HttpServerRequest0.HttpServerRequest>;
130
+ declare const toHttpApp: (group: InngestGroup.Any) => Effect.Effect<_$effect_unstable_http_HttpServerResponse0.HttpServerResponse, never, InngestClient | _$effect_unstable_http_HttpServerRequest0.HttpServerRequest | HttpClient.HttpClient>;
131
131
  /**
132
132
  * Create a standalone web handler from an InngestGroup.
133
133
  *
package/dist/HttpApi.d.ts CHANGED
@@ -49,13 +49,13 @@ declare const InngestApiGroup_base: HttpApiGroup.HttpApiGroup<"inngest", HttpApi
49
49
  readonly authentication_succeeded: Schema.optional<Schema.Union<readonly [Schema.Literal<false>, Schema.Null]>>;
50
50
  readonly capabilities: Schema.optional<Schema.$Record<Schema.String, Schema.String>>;
51
51
  readonly functions: Schema.optionalKey<Schema.$Array<Schema.Unknown>>;
52
- }>]>>, HttpApiEndpoint.Json<typeof FunctionNotFoundError | typeof InvalidRequestError | typeof SignatureError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"register", "PUT", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
52
+ }>]>>, HttpApiEndpoint.Json<typeof InvalidRequestError | typeof FunctionNotFoundError | typeof SignatureError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"register", "PUT", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
53
53
  readonly message: Schema.String;
54
54
  readonly modified: Schema.Boolean;
55
- }>>, HttpApiEndpoint.Json<typeof FunctionNotFoundError | typeof InvalidRequestError | typeof SignatureError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"execute", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<Schema.Struct<{
55
+ }>>, HttpApiEndpoint.Json<typeof InvalidRequestError | typeof FunctionNotFoundError | typeof SignatureError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"execute", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<Schema.Struct<{
56
56
  readonly fnId: Schema.String;
57
57
  readonly stepId: Schema.optional<Schema.String>;
58
- }>>, HttpApiEndpoint.Json<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Unknown>, HttpApiEndpoint.Json<typeof FunctionNotFoundError | typeof InvalidRequestError | typeof SignatureError>, never, never>, false>;
58
+ }>>, HttpApiEndpoint.Json<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Unknown>, HttpApiEndpoint.Json<typeof InvalidRequestError | typeof FunctionNotFoundError | typeof SignatureError>, never, never>, false>;
59
59
  /**
60
60
  * @since 0.1.0
61
61
  * @category api
package/dist/HttpApi.js CHANGED
@@ -56,7 +56,8 @@ const layerGroup = (api, group) => {
56
56
  group,
57
57
  fnId: query.fnId,
58
58
  urlStepId: query.stepId,
59
- body: payload
59
+ body: payload,
60
+ headers: request.headers
60
61
  })), Effect.map((r) => r.body)));
61
62
  })).pipe(Layer.provide(SignatureLive));
62
63
  };
@@ -0,0 +1 @@
1
+ import { Schema } from "effect";
@@ -0,0 +1,5 @@
1
+ import { Schema } from "effect";
2
+ //#region src/internal/checkpoint/Abort.ts
3
+ var StaleDispatch = class extends Schema.TaggedClass()("StaleDispatch", {}) {};
4
+ //#endregion
5
+ export { StaleDispatch };
@@ -1 +1 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Option } from "effect";
@@ -1,3 +1,4 @@
1
+ import { StaleDispatch } from "./Abort.js";
1
2
  import { Array, Clock, Duration, Effect, Option, Ref, Result } from "effect";
2
3
  //#region src/internal/checkpoint/State.ts
3
4
  const make = (args) => Effect.sync(() => {
@@ -5,12 +6,17 @@ const make = (args) => Effect.sync(() => {
5
6
  const plannedBuffer = Ref.makeUnsafe(Array.empty());
6
7
  const intervalStartedAt = Ref.makeUnsafe(Option.none());
7
8
  const runtimeExceeded = Ref.makeUnsafe(false);
9
+ const abort = Ref.makeUnsafe(Option.none());
8
10
  const maxIntervalMs = Duration.toMillis(args.config.maxInterval);
9
11
  const flushInner = Effect.gen(function* () {
10
12
  const steps = yield* Ref.modify(buffer, (current) => [current, Array.empty()]);
11
13
  if (steps.length === 0) return;
12
14
  const result = yield* Effect.result(args.checkpointAsync(steps));
13
15
  if (Result.isFailure(result)) {
16
+ if (result.failure.status === 409) {
17
+ yield* Ref.set(abort, Option.some(StaleDispatch.make({})));
18
+ return yield* Effect.interrupt;
19
+ }
14
20
  yield* Ref.update(buffer, (current) => [...steps, ...current]);
15
21
  return;
16
22
  }
@@ -47,7 +53,8 @@ const make = (args) => Effect.sync(() => {
47
53
  flush: flushInner,
48
54
  takeCompleted,
49
55
  markRuntimeExceeded: Ref.set(runtimeExceeded, true),
50
- isRuntimeExceeded: Ref.get(runtimeExceeded)
56
+ isRuntimeExceeded: Ref.get(runtimeExceeded),
57
+ abort: Ref.get(abort)
51
58
  };
52
59
  });
53
60
  //#endregion
@@ -21,6 +21,6 @@ var RetryAfterError = class extends Schema.TaggedErrorClass()("RetryAfterError",
21
21
  retryAfter: Schema.DurationFromMillis,
22
22
  cause: Schema.optional(Schema.Unknown)
23
23
  }) {};
24
- const isRetryAfterError = Predicate.isTagged("RetryAfterError");
24
+ const isRetryAfterError = Schema.is(RetryAfterError);
25
25
  //#endregion
26
26
  export { NonRetriableError, RetryAfterError, SendEventError, StepError, isNonRetriableError, isRetryAfterError, isStepError };
@@ -1,6 +1,7 @@
1
1
  import { GeneratorOpcode } from "../protocol.js";
2
2
  import { InngestConfig } from "../../Client.js";
3
3
  import { CurrentCheckpoint } from "../runtime/CheckpointContext.js";
4
+ import { StepError } from "../errors.js";
4
5
  import { StepCommandBus } from "../runtime/StepCommandBus.js";
5
6
  import { ExecutionFailure } from "./ExecutionFailure.js";
6
7
  import { RetryDisposition, base } from "./ExecutionHeaders.js";
@@ -23,7 +24,27 @@ const fromSuccess = (args) => Effect.gen(function* () {
23
24
  headers: args.headers
24
25
  })), Match.tag("CheckpointDeadlineElapsed", () => ExecutionResult.checkpointDeadlineOutsideCheckpoint(args)), Match.exhaustive);
25
26
  });
27
+ const fromCheckpointAbort = (args) => Match.value(args.abort).pipe(Match.tag("StaleDispatch", () => {
28
+ const error = new StepError({
29
+ stepId: "checkpoint",
30
+ message: "Stale dispatch: checkpoint returned 409",
31
+ noRetry: true
32
+ });
33
+ return ExecutionResult.userError({
34
+ error,
35
+ headers: args.headers,
36
+ disposition: RetryDisposition.fromError(error)
37
+ });
38
+ }), Match.exhaustive);
26
39
  const fromFailure = (args) => Effect.gen(function* () {
40
+ const checkpoint = yield* CurrentCheckpoint;
41
+ if (Cause.hasInterruptsOnly(args.cause) && Option.isSome(checkpoint)) {
42
+ const abort = yield* checkpoint.value.abort;
43
+ if (Option.isSome(abort)) return fromCheckpointAbort({
44
+ abort: abort.value,
45
+ headers: args.headers
46
+ });
47
+ }
27
48
  const commands = yield* (yield* StepCommandBus).takeSuspension();
28
49
  if (commands.suspendedCount > 0 && Cause.hasInterruptsOnly(args.cause)) return ExecutionResult.opcodesWithRetry({
29
50
  opcodes: commands.opcodes,
@@ -45,10 +66,18 @@ const fromFailure = (args) => Effect.gen(function* () {
45
66
  });
46
67
  });
47
68
  const fromExit = (exit) => Effect.gen(function* () {
48
- const config = yield* InngestConfig;
49
- const bus = yield* StepCommandBus;
50
- const headers = base(config);
51
- const planned = yield* bus.takePlanned();
69
+ const headers = base(yield* InngestConfig);
70
+ if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
71
+ const checkpoint = yield* CurrentCheckpoint;
72
+ if (Option.isSome(checkpoint)) {
73
+ const abort = yield* checkpoint.value.abort;
74
+ if (Option.isSome(abort)) return fromCheckpointAbort({
75
+ abort: abort.value,
76
+ headers
77
+ });
78
+ }
79
+ }
80
+ const planned = yield* (yield* StepCommandBus).takePlanned();
52
81
  if (planned.length > 0) return ExecutionResult.opcodes({
53
82
  opcodes: planned,
54
83
  headers
@@ -1,5 +1,5 @@
1
1
  import { resolveConfig } from "./checkpoint/Config.js";
2
- import { Headers, RegisterServerResponse, SDKRequestBody, SDKRequestContext, UserError } from "./protocol.js";
2
+ import { Headers as Headers$1, RegisterServerResponse, SDKRequestBody, SDKRequestContext, UserError } from "./protocol.js";
3
3
  import "./serve/Signature.js";
4
4
  import { InngestClient } from "../Client.js";
5
5
  import { schemaBodyJson, verifySignature } from "./serve/Request.js";
@@ -9,16 +9,17 @@ import * as HttpClient from "effect/unstable/http/HttpClient";
9
9
  import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
10
10
  import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
11
11
  import "effect/unstable/http/HttpServerRequest";
12
+ import * as Headers from "effect/unstable/http/Headers";
12
13
  //#region src/internal/handler.ts
13
14
  var InvalidRequestError = class extends Schema.TaggedErrorClass()("InvalidRequestError", { message: Schema.String }) {};
14
15
  const SDK_VERSION = "2.0.0";
15
16
  const baseHeaders = (framework) => ({
16
17
  "Content-Type": "application/json",
17
18
  "User-Agent": `effect-inngest:v${SDK_VERSION}`,
18
- [Headers.SDK]: `effect-inngest:v${SDK_VERSION}`,
19
- [Headers.SDKHandled]: "true",
20
- [Headers.RequestVersion]: "2",
21
- ...framework ? { [Headers.Framework]: framework } : {}
19
+ [Headers$1.SDK]: `effect-inngest:v${SDK_VERSION}`,
20
+ [Headers$1.SDKHandled]: "true",
21
+ [Headers$1.RequestVersion]: "2",
22
+ ...framework ? { [Headers$1.Framework]: framework } : {}
22
23
  });
23
24
  const buildServeUrl = (args) => {
24
25
  const url = new URL(args.requestUrl);
@@ -66,11 +67,11 @@ const handleRegistration = Effect.fn("effect-inngest/handler/handleRegistration"
66
67
  const framework = config.framework;
67
68
  const registerUrl = new URL("fn/register", client.apiBaseUrl).toString();
68
69
  const registerHeaders = baseHeaders(framework);
69
- delete registerHeaders[Headers.RequestVersion];
70
+ delete registerHeaders[Headers$1.RequestVersion];
70
71
  const request = HttpClientRequest.post(registerUrl).pipe(HttpClientRequest.setHeaders({
71
72
  ...registerHeaders,
72
73
  Authorization: `Bearer ${config.signingKey ?? ""}`,
73
- [Headers.SyncKind]: "out_of_band"
74
+ [Headers$1.SyncKind]: "out_of_band"
74
75
  }), HttpClientRequest.bodyJsonUnsafe({
75
76
  url: url.href,
76
77
  deployType: "ping",
@@ -99,7 +100,7 @@ const handleRegistration = Effect.fn("effect-inngest/handler/handleRegistration"
99
100
  status: 200,
100
101
  headers: {
101
102
  ...baseHeaders(config.framework),
102
- [Headers.SyncKind]: "out_of_band"
103
+ [Headers$1.SyncKind]: "out_of_band"
103
104
  },
104
105
  body: {
105
106
  message: "Successfully registered",
@@ -131,7 +132,7 @@ const handleExecution = Effect.fn("effect-inngest/handler/handleExecution")(func
131
132
  status: 500,
132
133
  headers: {
133
134
  ...baseHeaders(client.config.framework),
134
- [Headers.NoRetry]: "false"
135
+ [Headers$1.NoRetry]: "false"
135
136
  },
136
137
  body: UserError.make({
137
138
  name: "FunctionNotFoundError",
@@ -139,27 +140,39 @@ const handleExecution = Effect.fn("effect-inngest/handler/handleExecution")(func
139
140
  })
140
141
  };
141
142
  const requestedStepId = args.urlStepId === "step" ? void 0 : args.urlStepId;
143
+ const requestId = (args.headers ? Option.getOrUndefined(Headers.get(args.headers, Headers$1.RequestId)) : void 0) ?? args.body.ctx.request_id;
144
+ const rawGenerationId = args.headers ? Option.getOrUndefined(Headers.get(args.headers, Headers$1.GenerationId)) : void 0;
145
+ const parsedGenerationId = rawGenerationId ? Number(rawGenerationId) : void 0;
146
+ const generationId = Predicate.isNumber(parsedGenerationId) && Number.isInteger(parsedGenerationId) ? parsedGenerationId : args.body.ctx.generation_id;
147
+ const withExecutionHeaders = (ctx) => SDKRequestContext.make({
148
+ fn_id: ctx.fn_id,
149
+ run_id: ctx.run_id,
150
+ env: ctx.env,
151
+ step_id: requestedStepId ?? ctx.step_id,
152
+ attempt: ctx.attempt,
153
+ max_attempts: ctx.max_attempts,
154
+ qi_id: ctx.qi_id,
155
+ ...Predicate.isNotUndefined(requestId) ? { request_id: requestId } : {},
156
+ ...Predicate.isNotUndefined(generationId) ? { generation_id: generationId } : {},
157
+ disable_immediate_execution: ctx.disable_immediate_execution,
158
+ use_api: ctx.use_api,
159
+ stack: ctx.stack
160
+ });
142
161
  const effectiveBody = requestedStepId && requestedStepId !== args.body.ctx.step_id ? SDKRequestBody.make({
143
162
  event: args.body.event,
144
163
  events: args.body.events,
145
164
  steps: args.body.steps,
146
- ctx: SDKRequestContext.make({
147
- fn_id: args.body.ctx.fn_id,
148
- run_id: args.body.ctx.run_id,
149
- env: args.body.ctx.env,
150
- step_id: requestedStepId,
151
- attempt: args.body.ctx.attempt,
152
- max_attempts: args.body.ctx.max_attempts,
153
- qi_id: args.body.ctx.qi_id,
154
- ...Predicate.isNotUndefined(args.body.ctx.request_id) ? { request_id: args.body.ctx.request_id } : {},
155
- ...Predicate.isNotUndefined(args.body.ctx.generation_id) ? { generation_id: args.body.ctx.generation_id } : {},
156
- disable_immediate_execution: args.body.ctx.disable_immediate_execution,
157
- use_api: args.body.ctx.use_api,
158
- stack: args.body.ctx.stack
159
- }),
165
+ ctx: withExecutionHeaders(args.body.ctx),
160
166
  version: args.body.version,
161
167
  use_api: args.body.use_api
162
- }) : args.body;
168
+ }) : SDKRequestBody.make({
169
+ event: args.body.event,
170
+ events: args.body.events,
171
+ steps: args.body.steps,
172
+ ctx: withExecutionHeaders(args.body.ctx),
173
+ version: args.body.version,
174
+ use_api: args.body.use_api
175
+ });
163
176
  const checkpointConfig = !requestedStepId && effectiveBody.ctx.fn_id !== "" && !effectiveBody.ctx.disable_immediate_execution && effectiveBody.ctx.attempt === 0 ? Option.fromNullishOr(resolveConfig(fn.options.checkpointing, client.config.checkpointing)) : Option.none();
164
177
  const result = yield* Effect.provide(execute({
165
178
  fn,
@@ -90,6 +90,9 @@ const Headers = {
90
90
  SyncKind: "x-inngest-sync-kind",
91
91
  ServerKind: "X-Inngest-Server-Kind",
92
92
  ExpectedServerKind: "X-Inngest-Expected-Server-Kind",
93
+ RequestId: "x-request-id",
94
+ GenerationId: "x-inngest-generation-id",
95
+ JobId: "x-inngest-job-id",
93
96
  RunID: "X-Run-ID",
94
97
  Framework: "X-Inngest-Framework",
95
98
  Platform: "X-Inngest-Platform",
@@ -54,7 +54,8 @@ const toHttpApp = (group) => Effect.gen(function* () {
54
54
  group,
55
55
  fnId: params.fnId,
56
56
  urlStepId: params.stepId,
57
- body
57
+ body,
58
+ headers: request.headers
58
59
  });
59
60
  return yield* HttpServerResponse.json(result.body, {
60
61
  status: result.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-inngest",
3
- "version": "0.3.0-beta.3",
3
+ "version": "0.3.0-beta.4",
4
4
  "description": "Native Effect client library for Inngest - build durable, type-safe workflows",
5
5
  "keywords": [
6
6
  "background-jobs",
package/src/HttpApi.ts CHANGED
@@ -78,7 +78,13 @@ export const layerGroup = <ApiId extends string, Groups extends HttpApiGroup.Any
78
78
  .handleRaw("execute", ({ query, request }) =>
79
79
  InternalHandler.verifyAndParseRequestBody(request).pipe(
80
80
  Effect.flatMap((payload) =>
81
- InternalHandler.handleExecution({ group, fnId: query.fnId, urlStepId: query.stepId, body: payload }),
81
+ InternalHandler.handleExecution({
82
+ group,
83
+ fnId: query.fnId,
84
+ urlStepId: query.stepId,
85
+ body: payload,
86
+ headers: request.headers,
87
+ }),
82
88
  ),
83
89
  Effect.map((r) => r.body),
84
90
  ),
@@ -0,0 +1,5 @@
1
+ import { Schema } from "effect";
2
+
3
+ export class StaleDispatch extends Schema.TaggedClass<StaleDispatch>()("StaleDispatch", {}) {}
4
+
5
+ export type CheckpointAbort = StaleDispatch;
@@ -1,4 +1,5 @@
1
1
  import { Array as Arr, Clock, Duration, Effect, Option, Ref, Result } from "effect";
2
+ import * as CheckpointAbort from "./Abort.js";
2
3
  import type { CheckpointConfig } from "./Config.js";
3
4
  import type { CheckpointApiError } from "./Error.js";
4
5
  import type * as StepCommand from "../domain/StepCommand.js";
@@ -16,6 +17,7 @@ export interface CheckpointState {
16
17
  readonly takeCompleted: () => Effect.Effect<ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>>;
17
18
  readonly markRuntimeExceeded: Effect.Effect<void>;
18
19
  readonly isRuntimeExceeded: Effect.Effect<boolean>;
20
+ readonly abort: Effect.Effect<Option.Option<CheckpointAbort.CheckpointAbort>>;
19
21
  }
20
22
 
21
23
  export const make = (args: {
@@ -32,6 +34,7 @@ export const make = (args: {
32
34
  const plannedBuffer = Ref.makeUnsafe<ReadonlyArray<StepCommand.PlannedOpcode>>(Arr.empty());
33
35
  const intervalStartedAt = Ref.makeUnsafe<Option.Option<number>>(Option.none());
34
36
  const runtimeExceeded = Ref.makeUnsafe(false);
37
+ const abort = Ref.makeUnsafe<Option.Option<CheckpointAbort.CheckpointAbort>>(Option.none());
35
38
 
36
39
  const maxIntervalMs = Duration.toMillis(args.config.maxInterval);
37
40
 
@@ -50,6 +53,10 @@ export const make = (args: {
50
53
  }
51
54
  const result = yield* Effect.result(args.checkpointAsync(steps));
52
55
  if (Result.isFailure(result)) {
56
+ if (result.failure.status === 409) {
57
+ yield* Ref.set(abort, Option.some(CheckpointAbort.StaleDispatch.make({})));
58
+ return yield* Effect.interrupt;
59
+ }
53
60
  // Restore at the head so subsequent `completed` includes them in the 206.
54
61
  yield* Ref.update(buffer, (current) => [...steps, ...current]);
55
62
  return;
@@ -103,5 +110,6 @@ export const make = (args: {
103
110
  takeCompleted,
104
111
  markRuntimeExceeded: Ref.set(runtimeExceeded, true),
105
112
  isRuntimeExceeded: Ref.get(runtimeExceeded),
113
+ abort: Ref.get(abort),
106
114
  };
107
115
  });
@@ -31,6 +31,4 @@ export class RetryAfterError extends Schema.TaggedErrorClass<RetryAfterError>()(
31
31
  cause: Schema.optional(Schema.Unknown),
32
32
  }) {}
33
33
 
34
- export const isRetryAfterError: (u: unknown) => u is RetryAfterError = Predicate.isTagged("RetryAfterError") as (
35
- u: unknown,
36
- ) => u is RetryAfterError;
34
+ export const isRetryAfterError = Schema.is(RetryAfterError);
@@ -1,8 +1,10 @@
1
1
  import type * as Headers from "effect/unstable/http/Headers";
2
2
  import { Cause, Effect, Exit, Match, Option } from "effect";
3
3
  import { InngestConfig } from "../../Client.js";
4
+ import type * as CheckpointAbort from "../checkpoint/Abort.js";
4
5
  import { CurrentCheckpoint } from "../runtime/CheckpointContext.js";
5
6
  import { StepCommandBus } from "../runtime/StepCommandBus.js";
7
+ import { StepError } from "../errors.js";
6
8
  import * as Protocol from "../protocol.js";
7
9
  import { ExecutionFailure } from "./ExecutionFailure.js";
8
10
  import * as ExecutionHeaders from "./ExecutionHeaders.js";
@@ -34,8 +36,38 @@ const fromSuccess = (args: { readonly completion: HandlerRun.HandlerCompletion;
34
36
  );
35
37
  });
36
38
 
39
+ const fromCheckpointAbort = (args: {
40
+ readonly abort: CheckpointAbort.CheckpointAbort;
41
+ readonly headers: Headers.Headers;
42
+ }) =>
43
+ Match.value(args.abort).pipe(
44
+ Match.tag("StaleDispatch", () => {
45
+ const error = new StepError({
46
+ stepId: "checkpoint",
47
+ message: "Stale dispatch: checkpoint returned 409",
48
+ noRetry: true,
49
+ });
50
+
51
+ return ExecutionResult.userError({
52
+ error,
53
+ headers: args.headers,
54
+ disposition: ExecutionHeaders.RetryDisposition.fromError(error),
55
+ });
56
+ }),
57
+ Match.exhaustive,
58
+ );
59
+
37
60
  const fromFailure = (args: { readonly cause: Cause.Cause<unknown>; readonly headers: Headers.Headers }) =>
38
61
  Effect.gen(function* () {
62
+ const checkpoint = yield* CurrentCheckpoint;
63
+
64
+ if (Cause.hasInterruptsOnly(args.cause) && Option.isSome(checkpoint)) {
65
+ const abort = yield* checkpoint.value.abort;
66
+ if (Option.isSome(abort)) {
67
+ return fromCheckpointAbort({ abort: abort.value, headers: args.headers });
68
+ }
69
+ }
70
+
39
71
  const bus = yield* StepCommandBus;
40
72
  const commands = yield* bus.takeSuspension();
41
73
 
@@ -64,8 +96,19 @@ const fromFailure = (args: { readonly cause: Cause.Cause<unknown>; readonly head
64
96
  export const fromExit = (exit: Exit.Exit<HandlerRun.HandlerCompletion, unknown>) =>
65
97
  Effect.gen(function* () {
66
98
  const config = yield* InngestConfig;
67
- const bus = yield* StepCommandBus;
68
99
  const headers = ExecutionHeaders.base(config);
100
+
101
+ if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) {
102
+ const checkpoint = yield* CurrentCheckpoint;
103
+ if (Option.isSome(checkpoint)) {
104
+ const abort = yield* checkpoint.value.abort;
105
+ if (Option.isSome(abort)) {
106
+ return fromCheckpointAbort({ abort: abort.value, headers });
107
+ }
108
+ }
109
+ }
110
+
111
+ const bus = yield* StepCommandBus;
69
112
  const planned = yield* bus.takePlanned();
70
113
 
71
114
  if (planned.length > 0) {
@@ -1,13 +1,13 @@
1
1
  import * as HttpClient from "effect/unstable/http/HttpClient";
2
2
  import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
3
3
  import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
4
+ import * as Headers from "effect/unstable/http/Headers";
4
5
  import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
5
6
  import { Cause, Context, Effect, Option, Predicate, Schema } from "effect";
6
7
  import { InngestClient } from "../Client.js";
7
8
  import type { InngestFunction } from "../Function.js";
8
9
  import type { InngestGroup } from "../Group.js";
9
10
  import * as Checkpoint from "./checkpoint.js";
10
- import { SignatureError } from "./serve/Signature.js";
11
11
  import * as SdkRequest from "./serve/Request.js";
12
12
  import * as Protocol from "./protocol.js";
13
13
  export { SignatureError } from "./serve/Signature.js";
@@ -130,9 +130,6 @@ export const handleRegistration = Effect.fn("effect-inngest/handler/handleRegist
130
130
  }),
131
131
  );
132
132
 
133
- // Spec §4.3.1: PUT sync responds with `{ message: string; modified: boolean }`.
134
- // Spec §4.3.3 / §4.3.4: 500 on failure with `modified: false`; 200 on success
135
- // with the executor-supplied `modified` value (or `false` if omitted).
136
133
  return yield* Effect.gen(function* () {
137
134
  const response = yield* httpClient.execute(request).pipe(Effect.scoped);
138
135
  const responseBody = yield* HttpClientResponse.schemaBodyJson(Protocol.RegisterServerResponse)(response).pipe(
@@ -163,9 +160,6 @@ export const handleRegistration = Effect.fn("effect-inngest/handler/handleRegist
163
160
  } as HandlerResponse<typeof Protocol.RegisterResponse.Type>;
164
161
  }).pipe(
165
162
  Effect.catchCause((cause) => {
166
- // Network/transport failure (e.g. die from HttpClient mock). Per
167
- // §4.3.3 SHOULD return 500 with the body shape; never let the
168
- // toWebHandler default 500 wrapper escape.
169
163
  const errorOpt = Cause.findErrorOption(cause);
170
164
  const dieReason = cause.reasons.find(Cause.isDieReason);
171
165
  const message = Option.isSome(errorOpt)
@@ -197,6 +191,7 @@ export const handleExecution = Effect.fn("effect-inngest/handler/handleExecution
197
191
  readonly fnId: string;
198
192
  readonly urlStepId: string | undefined;
199
193
  readonly body: typeof Protocol.SDKRequestBody.Type;
194
+ readonly headers?: Headers.Headers;
200
195
  }) {
201
196
  const client = yield* InngestClient;
202
197
  const context = yield* Effect.context<never>();
@@ -209,9 +204,6 @@ export const handleExecution = Effect.fn("effect-inngest/handler/handleExecution
209
204
  const entry = fn ? (context.mapUnsafe.get(fn.key) as Handler<string> | undefined) : undefined;
210
205
 
211
206
  if (!fn || !entry) {
212
- // Spec §4.4.1: unknown function MUST return 500 with the error payload
213
- // at the response root (no { error: ... } envelope).
214
- // Spec §4.4.3 pair rule: 500 MUST pair with X-Inngest-No-Retry: false.
215
207
  return {
216
208
  status: 500 as const,
217
209
  headers: { ...baseHeaders(client.config.framework), [Protocol.Headers.NoRetry]: "false" },
@@ -220,41 +212,54 @@ export const handleExecution = Effect.fn("effect-inngest/handler/handleExecution
220
212
  }
221
213
 
222
214
  const requestedStepId = args.urlStepId === "step" ? undefined : args.urlStepId;
215
+ const headerRequestId = args.headers
216
+ ? Option.getOrUndefined(Headers.get(args.headers, Protocol.Headers.RequestId))
217
+ : undefined;
218
+ const requestId = headerRequestId ?? args.body.ctx.request_id;
219
+ const rawGenerationId = args.headers
220
+ ? Option.getOrUndefined(Headers.get(args.headers, Protocol.Headers.GenerationId))
221
+ : undefined;
222
+ const parsedGenerationId = rawGenerationId ? Number(rawGenerationId) : undefined;
223
+ const generationId =
224
+ Predicate.isNumber(parsedGenerationId) && Number.isInteger(parsedGenerationId)
225
+ ? parsedGenerationId
226
+ : args.body.ctx.generation_id;
227
+
228
+ const withExecutionHeaders = (ctx: typeof Protocol.SDKRequestContext.Type) =>
229
+ Protocol.SDKRequestContext.make({
230
+ fn_id: ctx.fn_id,
231
+ run_id: ctx.run_id,
232
+ env: ctx.env,
233
+ step_id: requestedStepId ?? ctx.step_id,
234
+ attempt: ctx.attempt,
235
+ max_attempts: ctx.max_attempts,
236
+ qi_id: ctx.qi_id,
237
+ ...(Predicate.isNotUndefined(requestId) ? { request_id: requestId } : {}),
238
+ ...(Predicate.isNotUndefined(generationId) ? { generation_id: generationId } : {}),
239
+ disable_immediate_execution: ctx.disable_immediate_execution,
240
+ use_api: ctx.use_api,
241
+ stack: ctx.stack,
242
+ });
223
243
 
224
- // Non-root URL stepId takes precedence over body.ctx.step_id. The root
225
- // `stepId=step` identifies the function run step, not a targeted child step.
226
244
  const effectiveBody =
227
245
  requestedStepId && requestedStepId !== args.body.ctx.step_id
228
246
  ? Protocol.SDKRequestBody.make({
229
247
  event: args.body.event,
230
248
  events: args.body.events,
231
249
  steps: args.body.steps,
232
- ctx: Protocol.SDKRequestContext.make({
233
- fn_id: args.body.ctx.fn_id,
234
- run_id: args.body.ctx.run_id,
235
- env: args.body.ctx.env,
236
- step_id: requestedStepId,
237
- attempt: args.body.ctx.attempt,
238
- max_attempts: args.body.ctx.max_attempts,
239
- qi_id: args.body.ctx.qi_id,
240
- ...(Predicate.isNotUndefined(args.body.ctx.request_id) ? { request_id: args.body.ctx.request_id } : {}),
241
- ...(Predicate.isNotUndefined(args.body.ctx.generation_id)
242
- ? { generation_id: args.body.ctx.generation_id }
243
- : {}),
244
- disable_immediate_execution: args.body.ctx.disable_immediate_execution,
245
- use_api: args.body.ctx.use_api,
246
- stack: args.body.ctx.stack,
247
- }),
250
+ ctx: withExecutionHeaders(args.body.ctx),
248
251
  version: args.body.version,
249
252
  use_api: args.body.use_api,
250
253
  })
251
- : args.body;
254
+ : Protocol.SDKRequestBody.make({
255
+ event: args.body.event,
256
+ events: args.body.events,
257
+ steps: args.body.steps,
258
+ ctx: withExecutionHeaders(args.body.ctx),
259
+ version: args.body.version,
260
+ use_api: args.body.use_api,
261
+ });
252
262
 
253
- // Checkpoint mode entry decision (spec §10):
254
- // - non-root URL stepId NOT set (we are not running a targeted child step)
255
- // - ctx.fn_id present (executor knows the function)
256
- // - ctx.disable_immediate_execution false (executor allowed it)
257
- // - resolved per-fn or per-client config not opted out
258
263
  const enterCheckpoint =
259
264
  !requestedStepId &&
260
265
  effectiveBody.ctx.fn_id !== "" &&
@@ -115,6 +115,9 @@ export const Headers = {
115
115
  SyncKind: "x-inngest-sync-kind",
116
116
  ServerKind: "X-Inngest-Server-Kind",
117
117
  ExpectedServerKind: "X-Inngest-Expected-Server-Kind",
118
+ RequestId: "x-request-id",
119
+ GenerationId: "x-inngest-generation-id",
120
+ JobId: "x-inngest-job-id",
118
121
  RunID: "X-Run-ID",
119
122
  Framework: "X-Inngest-Framework",
120
123
  Platform: "X-Inngest-Platform",
@@ -95,6 +95,7 @@ export const toHttpApp = (group: InngestGroup.Any) =>
95
95
  fnId: params.fnId,
96
96
  urlStepId: params.stepId,
97
97
  body,
98
+ headers: request.headers,
98
99
  });
99
100
 
100
101
  return yield* HttpServerResponse.json(result.body, {