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.
- package/dist/Client.d.ts +55 -24
- package/dist/Client.js +84 -25
- package/dist/Events.d.ts +36 -61
- package/dist/Events.js +5 -13
- package/dist/Function.d.ts +35 -15
- package/dist/Function.js +14 -8
- package/dist/Group.d.ts +5 -4
- package/dist/Group.js +26 -24
- package/dist/HttpApi.d.ts +49 -54
- package/dist/HttpApi.js +32 -17
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.js +1 -2
- package/dist/internal/checkpoint.d.ts +32 -0
- package/dist/internal/checkpoint.js +112 -0
- package/dist/internal/constants.js +1 -2
- package/dist/internal/driver.d.ts +1 -5
- package/dist/internal/driver.js +164 -52
- package/dist/internal/errors.d.ts +20 -27
- package/dist/internal/errors.js +29 -15
- package/dist/internal/handler.d.ts +4 -13
- package/dist/internal/handler.js +70 -43
- package/dist/internal/helpers.js +12 -9
- package/dist/internal/interrupts.d.ts +1 -2
- package/dist/internal/interrupts.js +1 -3
- package/dist/internal/memo.js +22 -20
- package/dist/internal/protocol.d.ts +28 -1
- package/dist/internal/protocol.js +84 -52
- package/dist/internal/signature.d.ts +5 -9
- package/dist/internal/signature.js +34 -18
- package/dist/internal/step.d.ts +5 -11
- package/dist/internal/step.js +48 -32
- package/package.json +26 -21
- package/src/Client.ts +244 -68
- package/src/Events.ts +3 -3
- package/src/Function.ts +52 -15
- package/src/Group.ts +39 -26
- package/src/HttpApi.ts +38 -27
- package/src/internal/checkpoint.ts +218 -0
- package/src/internal/driver.ts +241 -93
- package/src/internal/errors.ts +21 -14
- package/src/internal/handler.ts +185 -142
- package/src/internal/helpers.ts +9 -9
- package/src/internal/memo.ts +48 -33
- package/src/internal/protocol.ts +89 -45
- package/src/internal/signature.ts +76 -58
- package/src/internal/step.ts +83 -32
- package/dist/_virtual/rolldown_runtime.js +0 -18
package/src/internal/protocol.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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(
|
|
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.
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
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.
|
|
78
|
-
step_id: Schema.
|
|
79
|
-
attempt: Schema.
|
|
80
|
-
max_attempts: Schema.
|
|
81
|
-
stack:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
93
|
+
steps: Schema.Record(Schema.String, StepResult).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
|
|
91
94
|
ctx: SDKRequestContext,
|
|
92
|
-
version: Schema.
|
|
93
|
-
use_api: Schema.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
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.
|
|
205
|
+
mode: Schema.Literals(["cloud", "dev"]),
|
|
179
206
|
schema_version: Schema.Literal("2024-05-24"),
|
|
180
|
-
extra: Schema.optional(Schema.Record(
|
|
207
|
+
extra: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
181
208
|
});
|
|
182
209
|
|
|
183
|
-
export const IntrospectionUnauthenticated =
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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 =
|
|
192
|
-
|
|
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.
|
|
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.
|
|
17
|
-
reason: Schema.
|
|
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.
|
|
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.
|
|
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.
|
|
53
|
-
Schema.
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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.
|
|
111
|
-
Signature
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
});
|
package/src/internal/step.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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", () =>
|
|
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", () =>
|
|
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.
|
|
216
|
-
|
|
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.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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.
|
|
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) =>
|
|
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.
|
|
298
|
-
Effect.
|
|
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
|
-
|
|
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) =>
|
|
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 };
|