effect-inngest 0.1.3 → 0.3.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/README.md +89 -67
- package/dist/Client.d.ts +76 -28
- package/dist/Client.js +92 -30
- package/dist/Event.d.ts +43 -0
- package/dist/Event.js +46 -0
- package/dist/Events.d.ts +48 -76
- package/dist/Events.js +18 -23
- package/dist/Function.d.ts +46 -19
- package/dist/Function.js +32 -22
- package/dist/Group.d.ts +19 -8
- package/dist/Group.js +30 -72
- package/dist/HttpApi.d.ts +51 -56
- package/dist/HttpApi.js +38 -21
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -3
- package/dist/internal/checkpoint/Config.d.ts +10 -0
- package/dist/internal/checkpoint/Config.js +29 -0
- package/dist/internal/checkpoint/Error.d.ts +11 -0
- package/dist/internal/checkpoint/Error.js +8 -0
- package/dist/internal/checkpoint/State.d.ts +1 -0
- package/dist/internal/checkpoint/State.js +54 -0
- package/dist/internal/checkpoint.d.ts +2 -0
- package/dist/internal/codec/EventPayload.d.ts +6 -0
- package/dist/internal/codec/EventPayload.js +60 -0
- package/dist/internal/codec/StepResult.js +55 -0
- package/dist/internal/domain/ExecutionInput.d.ts +40 -0
- package/dist/internal/domain/ExecutionInput.js +57 -0
- package/dist/internal/domain/ExecutionSuspension.d.ts +34 -0
- package/dist/internal/domain/ExecutionSuspension.js +64 -0
- package/dist/internal/domain/FunctionDefinition.js +13 -0
- package/dist/internal/domain/Memo.d.ts +22 -0
- package/dist/internal/domain/Memo.js +18 -0
- package/dist/internal/domain/StepCommand.d.ts +79 -0
- package/dist/internal/domain/StepCommand.js +124 -0
- package/dist/internal/domain/StepInfo.d.ts +12 -0
- package/dist/internal/domain/StepInfo.js +10 -0
- package/dist/internal/domain/StepInput.d.ts +10 -0
- package/dist/internal/driver.js +15 -114
- package/dist/internal/errors.d.ts +21 -48
- package/dist/internal/errors.js +9 -44
- package/dist/internal/execution/CheckpointRun.js +25 -0
- package/dist/internal/execution/ExecutionFailure.js +19 -0
- package/dist/internal/execution/ExecutionHeaders.js +58 -0
- package/dist/internal/execution/ExecutionResponse.js +68 -0
- package/dist/internal/execution/ExecutionResult.js +56 -0
- package/dist/internal/execution/ExecutionScope.js +30 -0
- package/dist/internal/execution/HandlerRun.js +16 -0
- package/dist/internal/handler.d.ts +5 -16
- package/dist/internal/handler.js +128 -96
- package/dist/internal/protocol.d.ts +143 -1
- package/dist/internal/protocol.js +333 -138
- package/dist/internal/runtime/CheckpointContext.js +5 -0
- package/dist/internal/runtime/HandlerContext.d.ts +13 -0
- package/dist/internal/runtime/HandlerContext.js +19 -0
- package/dist/internal/runtime/HandlerFiberScope.d.ts +10 -0
- package/dist/internal/runtime/HandlerFiberScope.js +5 -0
- package/dist/internal/runtime/StepCommandBus.d.ts +27 -0
- package/dist/internal/runtime/StepCommandBus.js +76 -0
- package/dist/internal/runtime/StepIdentity.d.ts +27 -0
- package/dist/internal/runtime/StepIdentity.js +46 -0
- package/dist/internal/runtime/StepTools.d.ts +83 -0
- package/dist/internal/runtime/StepTools.js +76 -0
- package/dist/internal/runtime/steps/InvokeStep.js +43 -0
- package/dist/internal/runtime/steps/SendEventStep.js +46 -0
- package/dist/internal/runtime/steps/SleepStep.js +22 -0
- package/dist/internal/runtime/steps/SleepUntilStep.js +22 -0
- package/dist/internal/runtime/steps/StepRun.js +48 -0
- package/dist/internal/runtime/steps/WaitForEventStep.js +27 -0
- package/dist/internal/serve/HttpApp.js +71 -0
- package/dist/internal/serve/Request.js +23 -0
- package/dist/internal/serve/Signature.d.ts +11 -0
- package/dist/internal/serve/Signature.js +123 -0
- package/dist/internal/wire/Duration.js +19 -0
- package/dist/internal/wire/Timestamp.js +14 -0
- package/package.json +34 -22
- package/src/Client.ts +269 -91
- package/src/Event.ts +107 -0
- package/src/Events.ts +50 -30
- package/src/Function.ts +102 -46
- package/src/Group.ts +56 -108
- package/src/HttpApi.ts +40 -30
- package/src/index.ts +21 -11
- package/src/internal/checkpoint/Config.ts +74 -0
- package/src/internal/checkpoint/Error.ts +6 -0
- package/src/internal/checkpoint/State.ts +107 -0
- package/src/internal/checkpoint.ts +3 -0
- package/src/internal/codec/EventPayload.ts +98 -0
- package/src/internal/codec/StepResult.ts +95 -0
- package/src/internal/domain/ExecutionInput.ts +66 -0
- package/src/internal/domain/ExecutionSuspension.ts +79 -0
- package/src/internal/domain/FunctionDefinition.ts +30 -0
- package/src/internal/domain/Memo.ts +28 -0
- package/src/internal/domain/StepCommand.ts +166 -0
- package/src/internal/domain/StepInfo.ts +8 -0
- package/src/internal/domain/StepInput.ts +10 -0
- package/src/internal/driver.ts +27 -185
- package/src/internal/errors.ts +14 -108
- package/src/internal/execution/CheckpointRun.ts +33 -0
- package/src/internal/execution/ExecutionFailure.ts +19 -0
- package/src/internal/execution/ExecutionHeaders.ts +86 -0
- package/src/internal/execution/ExecutionResponse.ts +79 -0
- package/src/internal/execution/ExecutionResult.ts +57 -0
- package/src/internal/execution/ExecutionScope.ts +41 -0
- package/src/internal/execution/HandlerRun.ts +38 -0
- package/src/internal/handler.ts +222 -172
- package/src/internal/protocol.ts +289 -78
- package/src/internal/runtime/CheckpointContext.ts +7 -0
- package/src/internal/runtime/HandlerContext.ts +21 -0
- package/src/internal/runtime/HandlerFiberScope.ts +9 -0
- package/src/internal/runtime/StepCommandBus.ts +129 -0
- package/src/internal/runtime/StepIdentity.ts +67 -0
- package/src/internal/runtime/StepTools.ts +161 -0
- package/src/internal/runtime/steps/InvokeStep.ts +71 -0
- package/src/internal/runtime/steps/SendEventStep.ts +67 -0
- package/src/internal/runtime/steps/SleepStep.ts +34 -0
- package/src/internal/runtime/steps/SleepUntilStep.ts +34 -0
- package/src/internal/runtime/steps/StepRun.ts +95 -0
- package/src/internal/runtime/steps/WaitForEventStep.ts +55 -0
- package/src/internal/serve/HttpApp.ts +123 -0
- package/src/internal/serve/Request.ts +27 -0
- package/src/internal/serve/Signature.ts +170 -0
- package/src/internal/wire/Duration.ts +31 -0
- package/src/internal/wire/Timestamp.ts +11 -0
- package/dist/_virtual/rolldown_runtime.js +0 -18
- package/dist/internal/constants.js +0 -15
- package/dist/internal/driver.d.ts +0 -5
- package/dist/internal/helpers.js +0 -44
- package/dist/internal/interrupts.d.ts +0 -2
- package/dist/internal/interrupts.js +0 -45
- package/dist/internal/memo.js +0 -56
- package/dist/internal/signature.d.ts +0 -18
- package/dist/internal/signature.js +0 -97
- package/dist/internal/step.d.ts +0 -59
- package/dist/internal/step.js +0 -192
- package/src/internal/constants.ts +0 -11
- package/src/internal/helpers.ts +0 -58
- package/src/internal/interrupts.ts +0 -62
- package/src/internal/memo.ts +0 -73
- package/src/internal/signature.ts +0 -158
- package/src/internal/step.ts +0 -394
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { InngestClient } from "../../Client.js";
|
|
2
|
+
import { Array, Context, DateTime, Effect, Encoding, Layer, Option, Schema } from "effect";
|
|
3
|
+
import * as NodeCrypto from "node:crypto";
|
|
4
|
+
//#region src/internal/serve/Signature.ts
|
|
5
|
+
var SignatureError = class extends Schema.TaggedErrorClass()("SignatureError", {
|
|
6
|
+
reason: Schema.Literals([
|
|
7
|
+
"missing_header",
|
|
8
|
+
"invalid_format",
|
|
9
|
+
"expired",
|
|
10
|
+
"invalid_signature",
|
|
11
|
+
"missing_signing_key"
|
|
12
|
+
]),
|
|
13
|
+
message: Schema.String
|
|
14
|
+
}) {};
|
|
15
|
+
var SignatureConfig = class extends Schema.Class("effect-inngest/SignatureConfig")({
|
|
16
|
+
verification: Schema.Literals(["disabled", "required"]),
|
|
17
|
+
signingKey: Schema.OptionFromUndefinedOr(Schema.String),
|
|
18
|
+
signingKeyFallback: Schema.OptionFromUndefinedOr(Schema.String)
|
|
19
|
+
}) {};
|
|
20
|
+
var PreparedSigningKey = class PreparedSigningKey extends Schema.Class("effect-inngest/PreparedSigningKey")({ bytes: Schema.Uint8Array }) {
|
|
21
|
+
static decode = (signingKey) => Schema.decodeUnknownOption(Schema.Uint8ArrayFromHex)(signingKey.replace(/^signkey-\w+-/, "")).pipe(Option.map((bytes) => PreparedSigningKey.make({ bytes })));
|
|
22
|
+
sign = (body, timestampSeconds) => NodeCrypto.createHmac("sha256", this.bytes).update(body).update(String(timestampSeconds)).digest();
|
|
23
|
+
verifies = (body, header) => {
|
|
24
|
+
const expected = this.sign(body, header.timestampSeconds);
|
|
25
|
+
return expected.length === header.signature.length && NodeCrypto.timingSafeEqual(expected, header.signature);
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
var SignatureHeader = class SignatureHeader extends Schema.Class("effect-inngest/SignatureHeader")({
|
|
29
|
+
timestampSeconds: Schema.Number,
|
|
30
|
+
signature: Schema.Uint8Array
|
|
31
|
+
}) {
|
|
32
|
+
static decode = (header) => {
|
|
33
|
+
const params = new URLSearchParams(header);
|
|
34
|
+
return Schema.decodeUnknownEffect(SignatureParams)({
|
|
35
|
+
t: params.get("t") ?? "",
|
|
36
|
+
s: params.get("s") ?? ""
|
|
37
|
+
}).pipe(Effect.map(({ t, s }) => SignatureHeader.make({
|
|
38
|
+
timestampSeconds: t,
|
|
39
|
+
signature: s
|
|
40
|
+
})), Effect.mapError(() => SignatureError.make({
|
|
41
|
+
reason: "invalid_format",
|
|
42
|
+
message: `Invalid signature format: expected t=<int>&s=<64-hex>, got: ${header}`
|
|
43
|
+
})));
|
|
44
|
+
};
|
|
45
|
+
assertFresh = Effect.fn("effect-inngest/SignatureHeader/assertFresh")(function* () {
|
|
46
|
+
const now = yield* DateTime.now;
|
|
47
|
+
if (Math.abs(now.epochMilliseconds - this.timestampSeconds * 1e3) <= SIGNATURE_VALIDITY_WINDOW_MS) return;
|
|
48
|
+
return yield* SignatureError.make({
|
|
49
|
+
reason: "expired",
|
|
50
|
+
message: `Signature expired: timestamp ${this.timestampSeconds} is outside the validity window`
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
var SignedPayload = class extends Schema.Class("effect-inngest/SignedPayload")({
|
|
55
|
+
body: Schema.Uint8Array,
|
|
56
|
+
signature: Schema.OptionFromUndefinedOr(SignatureHeader)
|
|
57
|
+
}) {
|
|
58
|
+
requireSignature = Effect.fn("effect-inngest/SignedPayload/requireSignature")(function() {
|
|
59
|
+
return Effect.fromOption(this.signature).pipe(Effect.mapError(() => SignatureError.make({
|
|
60
|
+
reason: "missing_header",
|
|
61
|
+
message: "Missing Inngest signature header"
|
|
62
|
+
})));
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const SIGNATURE_VALIDITY_WINDOW_MS = 300 * 1e3;
|
|
66
|
+
const TimestampSeconds = Schema.NumberFromString.pipe(Schema.check(Schema.isInt(), Schema.isGreaterThan(0)));
|
|
67
|
+
const SignatureParams = Schema.Struct({
|
|
68
|
+
t: TimestampSeconds,
|
|
69
|
+
s: Schema.Uint8ArrayFromHex
|
|
70
|
+
});
|
|
71
|
+
const hashSigningKey = (signingKey) => Encoding.encodeHex(NodeCrypto.createHash("sha256").update(signingKey.replace(/^signkey-\w+-/, "")).digest());
|
|
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({
|
|
76
|
+
reason: "invalid_format",
|
|
77
|
+
message: "Invalid signing key"
|
|
78
|
+
});
|
|
79
|
+
if (Option.isSome(config.signingKeyFallback) && Option.isNone(fallbackSigningKey)) return yield* SignatureError.make({
|
|
80
|
+
reason: "invalid_format",
|
|
81
|
+
message: "Invalid signing key"
|
|
82
|
+
});
|
|
83
|
+
const signingKeys = Array.getSomes([signingKey, fallbackSigningKey]);
|
|
84
|
+
return {
|
|
85
|
+
verify: Effect.fn("effect-inngest/Signature/verify")(function* (payload) {
|
|
86
|
+
if (config.verification === "disabled") return;
|
|
87
|
+
if (Array.isArrayEmpty(signingKeys)) return yield* SignatureError.make({
|
|
88
|
+
reason: "missing_signing_key",
|
|
89
|
+
message: "No signing key configured for signature verification"
|
|
90
|
+
});
|
|
91
|
+
const header = yield* payload.requireSignature();
|
|
92
|
+
yield* header.assertFresh();
|
|
93
|
+
if (!signingKeys.some((key) => key.verifies(payload.body, header))) return yield* SignatureError.make({
|
|
94
|
+
reason: "invalid_signature",
|
|
95
|
+
message: "Invalid signature"
|
|
96
|
+
});
|
|
97
|
+
}),
|
|
98
|
+
sign: Effect.fn("effect-inngest/Signature/sign")(function* (body) {
|
|
99
|
+
const key = yield* Option.match(signingKey, {
|
|
100
|
+
onNone: () => SignatureError.make({
|
|
101
|
+
reason: "missing_signing_key",
|
|
102
|
+
message: "No signing key configured for signing"
|
|
103
|
+
}),
|
|
104
|
+
onSome: Effect.succeed
|
|
105
|
+
});
|
|
106
|
+
const now = yield* DateTime.now;
|
|
107
|
+
const timestampSeconds = Math.floor(now.epochMilliseconds / 1e3);
|
|
108
|
+
return `t=${timestampSeconds}&s=${Encoding.encodeHex(key.sign(body, timestampSeconds))}`;
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
}) }) {
|
|
112
|
+
static layer = (config) => Layer.effect(this, this.make(config));
|
|
113
|
+
};
|
|
114
|
+
const SignatureLive = Layer.effect(Signature, Effect.gen(function* () {
|
|
115
|
+
const client = yield* InngestClient;
|
|
116
|
+
return yield* Signature.make(SignatureConfig.make({
|
|
117
|
+
verification: client.mode === "dev" ? "disabled" : "required",
|
|
118
|
+
signingKey: Option.fromNullishOr(client.config.signingKey),
|
|
119
|
+
signingKeyFallback: Option.fromNullishOr(client.config.signingKeyFallback)
|
|
120
|
+
}));
|
|
121
|
+
}));
|
|
122
|
+
//#endregion
|
|
123
|
+
export { Signature, SignatureError, SignatureHeader, SignatureLive, SignedPayload, hashSigningKey };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Duration, Schema, SchemaGetter, SchemaTransformation } from "effect";
|
|
2
|
+
const InngestDuration = Schema.Duration.pipe(Schema.check(Schema.makeFilter((duration) => Duration.isFinite(duration) && !Duration.isNegative(duration) ? void 0 : "a finite non-negative duration"))).pipe(Schema.encodeTo(Schema.String, {
|
|
3
|
+
decode: SchemaTransformation.durationFromString.decode,
|
|
4
|
+
encode: SchemaGetter.transform((duration) => {
|
|
5
|
+
const parts = Duration.parts(duration);
|
|
6
|
+
const weeks = Math.floor(parts.days / 7);
|
|
7
|
+
const days = parts.days % 7;
|
|
8
|
+
return [
|
|
9
|
+
weeks > 0 ? `${weeks}w` : "",
|
|
10
|
+
days > 0 ? `${days}d` : "",
|
|
11
|
+
parts.hours > 0 ? `${parts.hours}h` : "",
|
|
12
|
+
parts.minutes > 0 ? `${parts.minutes}m` : "",
|
|
13
|
+
parts.seconds > 0 ? `${parts.seconds}s` : "",
|
|
14
|
+
parts.millis > 0 ? `${parts.millis}ms` : ""
|
|
15
|
+
].join("") || "0s";
|
|
16
|
+
})
|
|
17
|
+
}));
|
|
18
|
+
//#endregion
|
|
19
|
+
export { InngestDuration };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DateTime, Predicate, Schema, SchemaTransformation, identity } from "effect";
|
|
2
|
+
//#region src/internal/wire/Timestamp.ts
|
|
3
|
+
const TimestampInput = Schema.Union([
|
|
4
|
+
Schema.String,
|
|
5
|
+
Schema.Number,
|
|
6
|
+
Schema.DateValid
|
|
7
|
+
]);
|
|
8
|
+
const decode = (value) => Predicate.isString(value) ? value : DateTime.formatIso(DateTime.makeUnsafe(value));
|
|
9
|
+
const InngestTimestamp = TimestampInput.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.transform({
|
|
10
|
+
decode,
|
|
11
|
+
encode: identity
|
|
12
|
+
})));
|
|
13
|
+
//#endregion
|
|
14
|
+
export { InngestTimestamp };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-inngest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.0",
|
|
4
4
|
"description": "Native Effect client library for Inngest - build durable, type-safe workflows",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"background-jobs",
|
|
@@ -91,33 +91,45 @@
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
|
-
"build": "
|
|
95
|
-
"
|
|
96
|
-
"
|
|
94
|
+
"build": "vp pack",
|
|
95
|
+
"test": "vp test run",
|
|
96
|
+
"coverage": "vp test run --coverage",
|
|
97
|
+
"test:watch": "vp test",
|
|
98
|
+
"format": "vp fmt",
|
|
99
|
+
"lint": "vp lint --fix --type-aware --type-check --promise-plugin && sg scan src",
|
|
100
|
+
"ast-grep": "sg scan src",
|
|
97
101
|
"typecheck": "tsgo --noEmit",
|
|
98
|
-
"prepare": "effect-
|
|
102
|
+
"prepare": "vp config && effect-tsgo patch",
|
|
99
103
|
"changeset": "changeset",
|
|
100
104
|
"changeset:version": "changeset version",
|
|
101
105
|
"changeset:publish": "bun run build && changeset publish"
|
|
102
106
|
},
|
|
103
107
|
"devDependencies": {
|
|
104
|
-
"@
|
|
105
|
-
"@changesets/
|
|
106
|
-
"@
|
|
107
|
-
"@effect/
|
|
108
|
-
"@effect/
|
|
109
|
-
"@effect/
|
|
110
|
-
"@types/bun": "1.3.
|
|
111
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
108
|
+
"@ast-grep/cli": "^0.43.0",
|
|
109
|
+
"@changesets/changelog-github": "^0.6.0",
|
|
110
|
+
"@changesets/cli": "^2.31.0",
|
|
111
|
+
"@effect/platform-bun": "4.0.0-beta.83",
|
|
112
|
+
"@effect/tsgo": "0.14.3",
|
|
113
|
+
"@effect/vitest": "4.0.0-beta.83",
|
|
114
|
+
"@types/bun": "1.3.12",
|
|
115
|
+
"@typescript/native-preview": "^7.0.0-dev.20260420.1",
|
|
116
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
117
|
+
"effect": "4.0.0-beta.83",
|
|
118
|
+
"inngest": "^4.5.1",
|
|
119
|
+
"mockttp": "^4.4.2",
|
|
120
|
+
"typescript": "^5.9.3",
|
|
121
|
+
"vite-plus": "latest"
|
|
118
122
|
},
|
|
119
123
|
"peerDependencies": {
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
}
|
|
124
|
+
"effect": ">=4.0.0-beta.83",
|
|
125
|
+
"typescript": ">=5.9"
|
|
126
|
+
},
|
|
127
|
+
"overrides": {
|
|
128
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
129
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
130
|
+
},
|
|
131
|
+
"packageManager": "bun@1.3.13",
|
|
132
|
+
"trustedDependencies": [
|
|
133
|
+
"@ast-grep/cli"
|
|
134
|
+
]
|
|
123
135
|
}
|
package/src/Client.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @since 0.1.0
|
|
3
3
|
*/
|
|
4
|
-
import * as HttpClient from "
|
|
5
|
-
import * as HttpClientRequest from "
|
|
6
|
-
import * as HttpClientResponse from "
|
|
7
|
-
import
|
|
8
|
-
import type
|
|
9
|
-
import * as Context from "effect/Context";
|
|
10
|
-
import * as Effect from "effect/Effect";
|
|
11
|
-
import * as Layer from "effect/Layer";
|
|
12
|
-
import * as Option from "effect/Option";
|
|
13
|
-
import * as Predicate from "effect/Predicate";
|
|
14
|
-
import * as Schema from "effect/Schema";
|
|
4
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
5
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
6
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
7
|
+
import { Clock, Config, Context, Effect, Layer, Option, Predicate, Ref, Schedule, Schema } from "effect";
|
|
8
|
+
import { CheckpointApiError, type CheckpointingOption } from "./internal/checkpoint.js";
|
|
15
9
|
import * as Protocol from "./internal/protocol.js";
|
|
10
|
+
import { hashSigningKey } from "./internal/serve/Signature.js";
|
|
11
|
+
|
|
12
|
+
export type { CheckpointingOption } from "./internal/checkpoint.js";
|
|
13
|
+
export { CheckpointApiError } from "./internal/checkpoint.js";
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* @since 0.1.0
|
|
@@ -35,7 +33,7 @@ const SDK_VERSION = "2.0.0";
|
|
|
35
33
|
|
|
36
34
|
type ClientMode = "dev" | "cloud";
|
|
37
35
|
|
|
38
|
-
interface ClientConfig {
|
|
36
|
+
export interface ClientConfig {
|
|
39
37
|
/**
|
|
40
38
|
* The ID of this instance, most commonly a reference to the application it
|
|
41
39
|
* resides in.
|
|
@@ -111,6 +109,29 @@ interface ClientConfig {
|
|
|
111
109
|
* The path where the Inngest serve handler is mounted. Used for registration.
|
|
112
110
|
*/
|
|
113
111
|
readonly servePath?: string | undefined;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The framework adapter serving this app, e.g. "bun" or "nodejs". Used for
|
|
115
|
+
* protocol headers and registration metadata.
|
|
116
|
+
*/
|
|
117
|
+
readonly framework?: string | undefined;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether to use checkpointing by default for executions of functions
|
|
121
|
+
* created using this client.
|
|
122
|
+
*
|
|
123
|
+
* - `false` disables checkpointing for all functions on this client.
|
|
124
|
+
* - `true` enables checkpointing with safe defaults (`bufferedSteps: 1`,
|
|
125
|
+
* `maxInterval: 0`, `maxRuntime: 10s`). Steps are checkpointed to the
|
|
126
|
+
* Inngest API as they complete instead of yielding a 206 per step.
|
|
127
|
+
* - An object lets you tune `bufferedSteps`, `maxInterval`, `maxRuntime`.
|
|
128
|
+
*
|
|
129
|
+
* Per-function `checkpointing` overrides this setting. Defaults to `true`
|
|
130
|
+
* (checkpointing enabled).
|
|
131
|
+
*
|
|
132
|
+
* @default true
|
|
133
|
+
*/
|
|
134
|
+
readonly checkpointing?: CheckpointingOption | undefined;
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
export const EventPayload = Schema.Struct({
|
|
@@ -122,14 +143,14 @@ export const EventPayload = Schema.Struct({
|
|
|
122
143
|
});
|
|
123
144
|
|
|
124
145
|
const SendEventResponse = Schema.Struct({
|
|
125
|
-
ids: Schema.
|
|
146
|
+
ids: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
|
|
126
147
|
status: Schema.optional(Schema.Number),
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
type EventPayload = typeof EventPayload.Type;
|
|
130
151
|
type SendEventResponse = typeof SendEventResponse.Type;
|
|
131
152
|
|
|
132
|
-
class SendEventError extends Schema.
|
|
153
|
+
class SendEventError extends Schema.TaggedErrorClass<SendEventError>()("SendEventError", {
|
|
133
154
|
message: Schema.String,
|
|
134
155
|
events: Schema.Array(Schema.String),
|
|
135
156
|
}) {}
|
|
@@ -141,6 +162,26 @@ interface InngestClientService {
|
|
|
141
162
|
readonly eventBaseUrl: string;
|
|
142
163
|
readonly apiBaseUrl: string;
|
|
143
164
|
readonly sendEvent: (events: ReadonlyArray<EventPayload>) => Effect.Effect<SendEventResponse, SendEventError>;
|
|
165
|
+
/**
|
|
166
|
+
* Async checkpoint API call per spec §10.3.1.
|
|
167
|
+
*
|
|
168
|
+
* POSTs `{run_id, fn_id, qi_id, steps, ts}` to
|
|
169
|
+
* `{apiBaseUrl}/v1/checkpoint/{runId}/async` with a hashed-signing-key
|
|
170
|
+
* Bearer token. Retries 5xx responses with exponential backoff + jitter
|
|
171
|
+
* (5 attempts). On 401, falls back to the configured fallback signing key
|
|
172
|
+
* for the remainder of the client's lifetime (one-way switch).
|
|
173
|
+
*
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
readonly checkpointAsync: (args: {
|
|
177
|
+
readonly runId: string;
|
|
178
|
+
readonly fnId: string;
|
|
179
|
+
readonly qiId: string;
|
|
180
|
+
readonly requestId?: string;
|
|
181
|
+
readonly generationId?: number;
|
|
182
|
+
readonly requestStartedAt?: number;
|
|
183
|
+
readonly steps: ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>;
|
|
184
|
+
}) => Effect.Effect<void, CheckpointApiError>;
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
/**
|
|
@@ -149,7 +190,20 @@ interface InngestClientService {
|
|
|
149
190
|
* @since 0.1.0
|
|
150
191
|
* @category context
|
|
151
192
|
*/
|
|
152
|
-
export class InngestClient extends Context.
|
|
193
|
+
export class InngestClient extends Context.Service<InngestClient, InngestClientService>()(
|
|
194
|
+
"effect-inngest/InngestClient",
|
|
195
|
+
) {}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Static Inngest application configuration.
|
|
199
|
+
*
|
|
200
|
+
* This is the narrow dependency for internals that need app settings but not
|
|
201
|
+
* client behavior such as event sending or checkpoint API calls.
|
|
202
|
+
*
|
|
203
|
+
* @since 0.1.0
|
|
204
|
+
* @category context
|
|
205
|
+
*/
|
|
206
|
+
export class InngestConfig extends Context.Service<InngestConfig, ClientConfig>()("effect-inngest/InngestConfig") {}
|
|
153
207
|
|
|
154
208
|
const resolveMode = (config: ClientConfig): ClientMode => config.mode ?? "dev";
|
|
155
209
|
|
|
@@ -159,64 +213,181 @@ const resolveEventBaseUrl = (config: ClientConfig, mode: ClientMode): string =>
|
|
|
159
213
|
const resolveApiBaseUrl = (config: ClientConfig, mode: ClientMode): string =>
|
|
160
214
|
config.apiBaseUrl ?? (mode === "dev" ? DEFAULT_DEV_SERVER_URL : DEFAULT_API_BASE_URL);
|
|
161
215
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const eventNames = events.map((e) => e.name);
|
|
178
|
-
const now = Date.now();
|
|
179
|
-
|
|
180
|
-
const payloads = events.map((e) => ({
|
|
181
|
-
name: e.name,
|
|
182
|
-
data: e.data ?? {},
|
|
183
|
-
ts: e.ts ?? now,
|
|
184
|
-
id: e.id,
|
|
185
|
-
v: e.v,
|
|
186
|
-
}));
|
|
187
|
-
|
|
188
|
-
const request = HttpClientRequest.post(url).pipe(
|
|
189
|
-
HttpClientRequest.setHeaders({
|
|
190
|
-
"Content-Type": "application/json",
|
|
191
|
-
[Protocol.Headers.SDK]: `effect-ts:v${SDK_VERSION}`,
|
|
192
|
-
...(config.env ? { [Protocol.Headers.Env]: config.env } : {}),
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
216
|
+
const isRetriable = (error: CheckpointApiError): boolean => Predicate.isUndefined(error.status) || error.status >= 500;
|
|
217
|
+
|
|
218
|
+
const defaultCheckpointRetrySchedule: Schedule.Schedule<unknown, CheckpointApiError> = Schedule.exponential(
|
|
219
|
+
"100 millis",
|
|
220
|
+
2.0,
|
|
221
|
+
).pipe(
|
|
222
|
+
Schedule.jittered,
|
|
223
|
+
Schedule.both(Schedule.recurs(5)),
|
|
224
|
+
Schedule.while((m: Schedule.Metadata<unknown, CheckpointApiError>) => isRetriable(m.input)),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
export const CheckpointRetrySchedule = Context.Reference<Schedule.Schedule<unknown, CheckpointApiError>>(
|
|
228
|
+
"effect-inngest/internal/CheckpointRetrySchedule",
|
|
229
|
+
{ defaultValue: () => defaultCheckpointRetrySchedule },
|
|
230
|
+
);
|
|
195
231
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
232
|
+
const makeClient = (config: ClientConfig, httpClient: HttpClient.HttpClient): Effect.Effect<InngestClientService> =>
|
|
233
|
+
Effect.gen(function* () {
|
|
234
|
+
const mode = resolveMode(config);
|
|
235
|
+
const eventBaseUrl = resolveEventBaseUrl(config, mode);
|
|
236
|
+
const apiBaseUrl = resolveApiBaseUrl(config, mode);
|
|
237
|
+
const retrySchedule = yield* CheckpointRetrySchedule;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* One-way switch: once the fallback signing key succeeds for any
|
|
241
|
+
* checkpoint API call, all subsequent calls use it first. Per spec
|
|
242
|
+
* §10.3.4.
|
|
243
|
+
*/
|
|
244
|
+
const useFallbackKey = yield* Ref.make(false);
|
|
245
|
+
|
|
246
|
+
const sendEvent = Effect.fn("InngestClient.sendEvent")(function* (events: ReadonlyArray<EventPayload>) {
|
|
247
|
+
if (!config.eventKey && mode === "cloud") {
|
|
248
|
+
return yield* Effect.fail(
|
|
203
249
|
new SendEventError({
|
|
204
|
-
message:
|
|
205
|
-
events:
|
|
250
|
+
message: "Event key is required to send events in cloud mode",
|
|
251
|
+
events: events.map((e) => e.name),
|
|
206
252
|
}),
|
|
207
|
-
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const key = config.eventKey ?? "NO_EVENT_KEY_SET";
|
|
257
|
+
const url = new URL(`e/${key}`, eventBaseUrl).toString();
|
|
258
|
+
const eventNames = events.map((e) => e.name);
|
|
259
|
+
const now = yield* Clock.currentTimeMillis;
|
|
260
|
+
|
|
261
|
+
const payloads = events.map((e) => ({
|
|
262
|
+
name: e.name,
|
|
263
|
+
data: e.data ?? {},
|
|
264
|
+
ts: e.ts ?? now,
|
|
265
|
+
id: e.id,
|
|
266
|
+
v: e.v,
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
const request = HttpClientRequest.post(url).pipe(
|
|
270
|
+
HttpClientRequest.setHeaders({
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
[Protocol.Headers.SDK]: `effect-ts:v${SDK_VERSION}`,
|
|
273
|
+
...(config.env ? { [Protocol.Headers.Env]: config.env } : {}),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return yield* HttpClientRequest.schemaBodyJson(Schema.Array(EventPayload))(request, payloads).pipe(
|
|
278
|
+
Effect.flatMap(httpClient.execute),
|
|
279
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(SendEventResponse)),
|
|
280
|
+
Effect.map((response) => ({ ids: response.ids, status: response.status })),
|
|
281
|
+
Effect.scoped,
|
|
282
|
+
Effect.catch((error) =>
|
|
283
|
+
Effect.fail(
|
|
284
|
+
new SendEventError({
|
|
285
|
+
message: `Failed to send events: ${Predicate.hasProperty(error, "message") ? (error.message as string) : "Unknown error"}`,
|
|
286
|
+
events: eventNames,
|
|
287
|
+
}),
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Issue a checkpoint POST with the given signing key. Returns a tagged
|
|
295
|
+
* error on non-2xx so the caller can decide whether to retry, swap keys,
|
|
296
|
+
* or fall back.
|
|
297
|
+
*/
|
|
298
|
+
const checkpointRequest = (
|
|
299
|
+
runId: string,
|
|
300
|
+
body: string,
|
|
301
|
+
hashedSigningKey: string,
|
|
302
|
+
): Effect.Effect<void, CheckpointApiError> => {
|
|
303
|
+
const url = new URL(`v1/checkpoint/${runId}/async`, apiBaseUrl).toString();
|
|
304
|
+
const request = HttpClientRequest.post(url).pipe(
|
|
305
|
+
HttpClientRequest.setHeaders({
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
...(config.env ? { [Protocol.Headers.Env]: config.env } : {}),
|
|
308
|
+
}),
|
|
309
|
+
HttpClientRequest.bearerToken(hashedSigningKey),
|
|
310
|
+
HttpClientRequest.bodyText(body, "application/json"),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return httpClient.execute(request).pipe(
|
|
314
|
+
Effect.scoped,
|
|
315
|
+
Effect.catch((error) =>
|
|
316
|
+
Effect.fail(
|
|
317
|
+
new CheckpointApiError({
|
|
318
|
+
message: `Network error issuing checkpoint: ${Predicate.hasProperty(error, "message") ? (error.message as string) : String(error)}`,
|
|
319
|
+
}),
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
Effect.flatMap((response) => {
|
|
323
|
+
if (response.status >= 200 && response.status < 300) {
|
|
324
|
+
return Effect.void;
|
|
325
|
+
}
|
|
326
|
+
return Effect.fail(
|
|
327
|
+
new CheckpointApiError({
|
|
328
|
+
message: `Checkpoint API returned ${response.status}`,
|
|
329
|
+
status: response.status,
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const checkpointAsync: InngestClientService["checkpointAsync"] = Effect.fn("InngestClient.checkpointAsync")(
|
|
337
|
+
function* (args) {
|
|
338
|
+
const signingKey = config.signingKey;
|
|
339
|
+
if (!signingKey && mode !== "dev") {
|
|
340
|
+
return yield* Effect.fail(
|
|
341
|
+
new CheckpointApiError({ message: "No signing key configured for checkpoint API" }),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const now = yield* Clock.currentTimeMillis;
|
|
346
|
+
const body = JSON.stringify({
|
|
347
|
+
run_id: args.runId,
|
|
348
|
+
fn_id: args.fnId,
|
|
349
|
+
qi_id: args.qiId,
|
|
350
|
+
request_id: args.requestId,
|
|
351
|
+
generation_id: args.generationId,
|
|
352
|
+
request_started_at: args.requestStartedAt,
|
|
353
|
+
steps: args.steps,
|
|
354
|
+
ts: now,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const fallbackKey = config.signingKeyFallback;
|
|
358
|
+
|
|
359
|
+
// Single attempt: tries the chosen key first; on 401 with primary,
|
|
360
|
+
// tries the fallback once and flips the Ref.
|
|
361
|
+
const attempt: Effect.Effect<void, CheckpointApiError> = Ref.get(useFallbackKey).pipe(
|
|
362
|
+
Effect.flatMap((usingFallback) => {
|
|
363
|
+
const primaryKey = usingFallback && fallbackKey ? fallbackKey : signingKey;
|
|
364
|
+
const primaryToken = primaryKey ? hashSigningKey(primaryKey) : "";
|
|
365
|
+
return checkpointRequest(args.runId, body, primaryToken).pipe(
|
|
366
|
+
Effect.catch((error) =>
|
|
367
|
+
error.status === 401 && !usingFallback && fallbackKey
|
|
368
|
+
? checkpointRequest(args.runId, body, hashSigningKey(fallbackKey)).pipe(
|
|
369
|
+
Effect.andThen(Ref.set(useFallbackKey, true)),
|
|
370
|
+
)
|
|
371
|
+
: Effect.fail(error),
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
return yield* attempt.pipe(Effect.retry(retrySchedule));
|
|
378
|
+
},
|
|
208
379
|
);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
[TypeId]: TypeId,
|
|
383
|
+
config,
|
|
384
|
+
mode,
|
|
385
|
+
eventBaseUrl,
|
|
386
|
+
apiBaseUrl,
|
|
387
|
+
sendEvent,
|
|
388
|
+
checkpointAsync,
|
|
389
|
+
};
|
|
390
|
+
});
|
|
220
391
|
|
|
221
392
|
/**
|
|
222
393
|
* Create an InngestClient layer from a config.
|
|
@@ -231,10 +402,16 @@ const makeClient = (config: ClientConfig, httpClient: HttpClient.HttpClient): In
|
|
|
231
402
|
* })
|
|
232
403
|
* ```
|
|
233
404
|
*/
|
|
234
|
-
export const layer = (config: ClientConfig): Layer.Layer<InngestClient, never, HttpClient.HttpClient> =>
|
|
235
|
-
Layer.
|
|
236
|
-
|
|
237
|
-
|
|
405
|
+
export const layer = (config: ClientConfig): Layer.Layer<InngestClient | InngestConfig, never, HttpClient.HttpClient> =>
|
|
406
|
+
Layer.mergeAll(
|
|
407
|
+
Layer.succeed(InngestConfig, config),
|
|
408
|
+
Layer.effect(
|
|
409
|
+
InngestClient,
|
|
410
|
+
Effect.gen(function* () {
|
|
411
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
412
|
+
return yield* makeClient(config, httpClient);
|
|
413
|
+
}),
|
|
414
|
+
),
|
|
238
415
|
);
|
|
239
416
|
|
|
240
417
|
/**
|
|
@@ -244,13 +421,13 @@ export const layer = (config: ClientConfig): Layer.Layer<InngestClient, never, H
|
|
|
244
421
|
* @category layers
|
|
245
422
|
*/
|
|
246
423
|
export const layerConfig = (
|
|
247
|
-
config: Config.
|
|
248
|
-
): Layer.Layer<InngestClient,
|
|
249
|
-
Layer.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
),
|
|
424
|
+
config: Config.Wrap<ClientConfig>,
|
|
425
|
+
): Layer.Layer<InngestClient | InngestConfig, Config.ConfigError, HttpClient.HttpClient> =>
|
|
426
|
+
Layer.unwrap(
|
|
427
|
+
Effect.gen(function* () {
|
|
428
|
+
const resolvedConfig = yield* Config.unwrap(config);
|
|
429
|
+
return layer(resolvedConfig);
|
|
430
|
+
}),
|
|
254
431
|
);
|
|
255
432
|
|
|
256
433
|
/**
|
|
@@ -266,14 +443,15 @@ export const layerConfig = (
|
|
|
266
443
|
* @since 0.1.0
|
|
267
444
|
* @category layers
|
|
268
445
|
*/
|
|
269
|
-
export const layerFromEnv: Layer.Layer<InngestClient,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
Config.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
)
|
|
446
|
+
export const layerFromEnv: Layer.Layer<InngestClient | InngestConfig, Config.ConfigError, HttpClient.HttpClient> =
|
|
447
|
+
layerConfig(
|
|
448
|
+
Config.all({
|
|
449
|
+
id: Config.string("INNGEST_APP_ID").pipe(Config.withDefault("app")),
|
|
450
|
+
eventKey: Config.string("INNGEST_EVENT_KEY").pipe(Config.option, Config.map(Option.getOrUndefined)),
|
|
451
|
+
signingKey: Config.string("INNGEST_SIGNING_KEY").pipe(Config.option, Config.map(Option.getOrUndefined)),
|
|
452
|
+
signingKeyFallback: Config.string("INNGEST_SIGNING_KEY_FALLBACK").pipe(
|
|
453
|
+
Config.option,
|
|
454
|
+
Config.map(Option.getOrUndefined),
|
|
455
|
+
),
|
|
456
|
+
}),
|
|
457
|
+
);
|