effect-inngest 0.3.0-beta.2 → 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/HttpApi.js +2 -1
- package/dist/internal/checkpoint/Abort.d.ts +1 -0
- package/dist/internal/checkpoint/Abort.js +5 -0
- package/dist/internal/checkpoint/State.d.ts +1 -1
- package/dist/internal/checkpoint/State.js +8 -1
- package/dist/internal/errors.js +1 -1
- package/dist/internal/execution/ExecutionResponse.js +33 -4
- package/dist/internal/handler.js +37 -24
- package/dist/internal/protocol.js +3 -0
- package/dist/internal/serve/HttpApp.js +2 -1
- package/dist/internal/serve/Signature.js +9 -6
- package/package.json +1 -1
- package/src/HttpApi.ts +7 -1
- package/src/internal/checkpoint/Abort.ts +5 -0
- package/src/internal/checkpoint/State.ts +8 -0
- package/src/internal/errors.ts +1 -3
- package/src/internal/execution/ExecutionResponse.ts +44 -1
- package/src/internal/handler.ts +39 -34
- package/src/internal/protocol.ts +3 -0
- package/src/internal/serve/HttpApp.ts +1 -0
- package/src/internal/serve/Signature.ts +20 -7
package/dist/HttpApi.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
@@ -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
|
package/dist/internal/errors.js
CHANGED
|
@@ -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 =
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
package/dist/internal/handler.js
CHANGED
|
@@ -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:
|
|
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
|
-
}) :
|
|
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",
|
|
@@ -70,13 +70,13 @@ const SignatureParams = Schema.Struct({
|
|
|
70
70
|
});
|
|
71
71
|
const hashSigningKey = (signingKey) => Encoding.encodeHex(NodeCrypto.createHash("sha256").update(signingKey.replace(/^signkey-\w+-/, "")).digest());
|
|
72
72
|
var Signature = class extends Context.Service()("effect-inngest/Signature", { make: (config) => Effect.gen(function* () {
|
|
73
|
-
const signingKey = config.signingKey.pipe(Option.flatMap(PreparedSigningKey.decode));
|
|
74
|
-
const fallbackSigningKey = config.signingKeyFallback.pipe(Option.flatMap(PreparedSigningKey.decode));
|
|
75
|
-
if (Option.isSome(config.signingKey) && Option.isNone(signingKey)) return yield* SignatureError.make({
|
|
73
|
+
const signingKey = config.verification === "disabled" ? Option.none() : config.signingKey.pipe(Option.flatMap(PreparedSigningKey.decode));
|
|
74
|
+
const fallbackSigningKey = config.verification === "disabled" ? Option.none() : config.signingKeyFallback.pipe(Option.flatMap(PreparedSigningKey.decode));
|
|
75
|
+
if (config.verification === "required" && Option.isSome(config.signingKey) && Option.isNone(signingKey)) return yield* SignatureError.make({
|
|
76
76
|
reason: "invalid_format",
|
|
77
77
|
message: "Invalid signing key"
|
|
78
78
|
});
|
|
79
|
-
if (Option.isSome(config.signingKeyFallback) && Option.isNone(fallbackSigningKey)) return yield* SignatureError.make({
|
|
79
|
+
if (config.verification === "required" && Option.isSome(config.signingKeyFallback) && Option.isNone(fallbackSigningKey)) return yield* SignatureError.make({
|
|
80
80
|
reason: "invalid_format",
|
|
81
81
|
message: "Invalid signing key"
|
|
82
82
|
});
|
|
@@ -96,12 +96,15 @@ var Signature = class extends Context.Service()("effect-inngest/Signature", { ma
|
|
|
96
96
|
});
|
|
97
97
|
}),
|
|
98
98
|
sign: Effect.fn("effect-inngest/Signature/sign")(function* (body) {
|
|
99
|
-
const key = yield* Option.match(signingKey, {
|
|
99
|
+
const key = yield* Option.match(config.signingKey, {
|
|
100
100
|
onNone: () => SignatureError.make({
|
|
101
101
|
reason: "missing_signing_key",
|
|
102
102
|
message: "No signing key configured for signing"
|
|
103
103
|
}),
|
|
104
|
-
onSome: Effect.
|
|
104
|
+
onSome: (key) => Effect.fromOption(PreparedSigningKey.decode(key)).pipe(Effect.mapError(() => SignatureError.make({
|
|
105
|
+
reason: "invalid_format",
|
|
106
|
+
message: "Invalid signing key"
|
|
107
|
+
})))
|
|
105
108
|
});
|
|
106
109
|
const now = yield* DateTime.now;
|
|
107
110
|
const timestampSeconds = Math.floor(now.epochMilliseconds / 1e3);
|
package/package.json
CHANGED
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({
|
|
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
|
),
|
|
@@ -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
|
});
|
package/src/internal/errors.ts
CHANGED
|
@@ -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
|
|
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) {
|
package/src/internal/handler.ts
CHANGED
|
@@ -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:
|
|
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
|
-
:
|
|
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 !== "" &&
|
package/src/internal/protocol.ts
CHANGED
|
@@ -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",
|
|
@@ -100,13 +100,23 @@ export const hashSigningKey = (signingKey: string): string =>
|
|
|
100
100
|
export class Signature extends Context.Service<Signature, SignatureService>()("effect-inngest/Signature", {
|
|
101
101
|
make: (config: SignatureConfig) =>
|
|
102
102
|
Effect.gen(function* () {
|
|
103
|
-
const signingKey =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
const signingKey =
|
|
104
|
+
config.verification === "disabled"
|
|
105
|
+
? Option.none<PreparedSigningKey>()
|
|
106
|
+
: config.signingKey.pipe(Option.flatMap(PreparedSigningKey.decode));
|
|
107
|
+
const fallbackSigningKey =
|
|
108
|
+
config.verification === "disabled"
|
|
109
|
+
? Option.none<PreparedSigningKey>()
|
|
110
|
+
: config.signingKeyFallback.pipe(Option.flatMap(PreparedSigningKey.decode));
|
|
111
|
+
|
|
112
|
+
if (config.verification === "required" && Option.isSome(config.signingKey) && Option.isNone(signingKey)) {
|
|
107
113
|
return yield* SignatureError.make({ reason: "invalid_format", message: "Invalid signing key" });
|
|
108
114
|
}
|
|
109
|
-
if (
|
|
115
|
+
if (
|
|
116
|
+
config.verification === "required" &&
|
|
117
|
+
Option.isSome(config.signingKeyFallback) &&
|
|
118
|
+
Option.isNone(fallbackSigningKey)
|
|
119
|
+
) {
|
|
110
120
|
return yield* SignatureError.make({ reason: "invalid_format", message: "Invalid signing key" });
|
|
111
121
|
}
|
|
112
122
|
|
|
@@ -134,13 +144,16 @@ export class Signature extends Context.Service<Signature, SignatureService>()("e
|
|
|
134
144
|
});
|
|
135
145
|
|
|
136
146
|
const sign = Effect.fn("effect-inngest/Signature/sign")(function* (body: Uint8Array) {
|
|
137
|
-
const key = yield* Option.match(signingKey, {
|
|
147
|
+
const key = yield* Option.match(config.signingKey, {
|
|
138
148
|
onNone: () =>
|
|
139
149
|
SignatureError.make({
|
|
140
150
|
reason: "missing_signing_key",
|
|
141
151
|
message: "No signing key configured for signing",
|
|
142
152
|
}),
|
|
143
|
-
onSome:
|
|
153
|
+
onSome: (key) =>
|
|
154
|
+
Effect.fromOption(PreparedSigningKey.decode(key)).pipe(
|
|
155
|
+
Effect.mapError(() => SignatureError.make({ reason: "invalid_format", message: "Invalid signing key" })),
|
|
156
|
+
),
|
|
144
157
|
});
|
|
145
158
|
|
|
146
159
|
const now = yield* DateTime.now;
|