effect-inngest 0.1.3 → 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 +84 -31
- package/dist/_virtual/rolldown_runtime.js +0 -18
package/dist/internal/step.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { formatTimestamp, timeStr } from "./helpers.js";
|
|
2
|
+
import { stepRun } from "./protocol.js";
|
|
2
3
|
import { InngestClient } from "../Client.js";
|
|
3
4
|
import { SendEventError, StepError, isNonRetriableError, isRetryAfterError } from "./errors.js";
|
|
4
|
-
import {
|
|
5
|
+
import { errorInterrupt, invokeInterrupt, plannedInterrupt, runInterrupt, sleepInterrupt, waitForEventInterrupt } from "./interrupts.js";
|
|
5
6
|
import { OtelAttributes } from "./constants.js";
|
|
6
7
|
import { decodeMemo } from "./memo.js";
|
|
7
8
|
import * as Duration from "effect/Duration";
|
|
8
|
-
import * as Context from "effect/Context";
|
|
9
9
|
import * as Effect from "effect/Effect";
|
|
10
10
|
import * as Option from "effect/Option";
|
|
11
|
+
import * as Ref from "effect/Ref";
|
|
11
12
|
import * as Schema from "effect/Schema";
|
|
12
13
|
import * as Predicate from "effect/Predicate";
|
|
13
|
-
import * as Arr from "effect/Array";
|
|
14
14
|
import * as HashMap from "effect/HashMap";
|
|
15
|
-
import * as Match from "effect/Match";
|
|
16
|
-
import * as Ref from "effect/Ref";
|
|
17
15
|
import { pipe } from "effect/Function";
|
|
18
|
-
|
|
16
|
+
import * as Arr from "effect/Array";
|
|
17
|
+
import * as Match from "effect/Match";
|
|
19
18
|
//#region src/internal/step.ts
|
|
20
19
|
/**
|
|
21
20
|
* Internal step tools implementation.
|
|
@@ -40,10 +39,9 @@ const errorOtelAttributes = (err) => {
|
|
|
40
39
|
};
|
|
41
40
|
const encodeTaggedEvent = (event) => {
|
|
42
41
|
const ctor = event.constructor;
|
|
43
|
-
if (Schema.isSchema(ctor)) return Schema.
|
|
42
|
+
if (Schema.isSchema(ctor)) return Schema.encodeEffect(ctor)(event).pipe(Effect.orElseSucceed(() => event));
|
|
44
43
|
return Effect.succeed(event);
|
|
45
44
|
};
|
|
46
|
-
var Step = class extends Context.Tag("effect-inngest/Step")() {};
|
|
47
45
|
const normalizeOpts = (opts) => typeof opts === "string" ? {
|
|
48
46
|
id: opts,
|
|
49
47
|
name: opts
|
|
@@ -57,7 +55,7 @@ const stepError = (stepId, message, opts) => Effect.fail(StepError.make({
|
|
|
57
55
|
noRetry: opts?.noRetry,
|
|
58
56
|
cause: opts?.cause
|
|
59
57
|
}));
|
|
60
|
-
const createStepTools = (request, appName, stepIdCounts) => {
|
|
58
|
+
const createStepTools = (request, appName, stepIdCounts, checkpoint = Option.none()) => {
|
|
61
59
|
const ctx = request.ctx;
|
|
62
60
|
const steps = request.steps;
|
|
63
61
|
const getInfo = (opts) => {
|
|
@@ -74,30 +72,40 @@ const createStepTools = (request, appName, stepIdCounts) => {
|
|
|
74
72
|
const getMemo = (hash) => decodeMemo(steps[hash]);
|
|
75
73
|
const canExecute = (hash) => ctx.step_id === hash || ctx.step_id === "step";
|
|
76
74
|
const isBlocked = (hash) => ctx.disable_immediate_execution && ctx.step_id !== hash;
|
|
77
|
-
const
|
|
75
|
+
const flushIfCheckpoint = Option.match(checkpoint, {
|
|
76
|
+
onNone: () => Effect.void,
|
|
77
|
+
onSome: (state) => state.flush
|
|
78
|
+
});
|
|
79
|
+
const sleep = (opts, duration) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", () => Effect.void), Match.tag("MemoTimeout", () => Effect.void), Match.tag("MemoError", () => Effect.void), Match.tag("MemoInput", () => Effect.void), Match.tag("MemoNone", () => Effect.andThen(flushIfCheckpoint, Effect.fail(sleepInterrupt({
|
|
78
80
|
info,
|
|
79
81
|
duration: timeStr(duration)
|
|
80
|
-
})))
|
|
81
|
-
|
|
82
|
+
}))).pipe(Effect.withSpan(`inngest.step/sleep/${info.id}`, { attributes: {
|
|
83
|
+
[OtelAttributes.StepId]: info.id,
|
|
84
|
+
[OtelAttributes.StepType]: "sleep"
|
|
85
|
+
} }))), Match.exhaustive));
|
|
86
|
+
const sleepUntil = (opts, timestamp) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", () => Effect.void), Match.tag("MemoTimeout", () => Effect.void), Match.tag("MemoError", () => Effect.void), Match.tag("MemoInput", () => Effect.void), Match.tag("MemoNone", () => Effect.andThen(flushIfCheckpoint, Effect.fail(sleepInterrupt({
|
|
82
87
|
info,
|
|
83
88
|
duration: formatTimestamp(timestamp)
|
|
84
|
-
})))
|
|
89
|
+
}))).pipe(Effect.withSpan(`inngest.step/sleepUntil/${info.id}`, { attributes: {
|
|
90
|
+
[OtelAttributes.StepId]: info.id,
|
|
91
|
+
[OtelAttributes.StepType]: "sleepUntil"
|
|
92
|
+
} }))), Match.exhaustive));
|
|
85
93
|
const waitForEvent = (opts, event, options) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => {
|
|
86
94
|
if (data == null) return Effect.succeed(Option.none());
|
|
87
95
|
const event = data;
|
|
88
96
|
const payload = event.data !== void 0 ? event.data : data;
|
|
89
97
|
if (payload == null) return Effect.succeed(Option.none());
|
|
90
98
|
return Effect.succeed(Option.some(payload));
|
|
91
|
-
}), Match.tag("MemoTimeout", () => Effect.succeed(Option.none())), Match.tag("MemoError", () => Effect.succeed(Option.none())), Match.tag("MemoInput", () => Effect.succeed(Option.none())), Match.tag("MemoNone", () => Effect.fail(waitForEventInterrupt({
|
|
99
|
+
}), Match.tag("MemoTimeout", () => Effect.succeed(Option.none())), Match.tag("MemoError", () => Effect.succeed(Option.none())), Match.tag("MemoInput", () => Effect.succeed(Option.none())), Match.tag("MemoNone", () => Effect.andThen(flushIfCheckpoint, Effect.fail(waitForEventInterrupt({
|
|
92
100
|
info,
|
|
93
|
-
event: event.
|
|
101
|
+
event: event.identifier,
|
|
94
102
|
timeout: timeStr(options.timeout),
|
|
95
103
|
if: options.if
|
|
96
|
-
}))), Match.exhaustive));
|
|
104
|
+
})))), Match.exhaustive));
|
|
97
105
|
const invoke = (opts, options) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Invoke failed", { cause: error })), Match.tag("MemoTimeout", () => stepError(info.id, "Invoke timed out", { noRetry: true })), Match.tag("MemoInput", () => Effect.succeed(void 0)), Match.tag("MemoNone", () => {
|
|
98
106
|
const rawData = Predicate.hasProperty(options, "data") ? options.data : void 0;
|
|
99
107
|
const encodeData = rawData ? encodeTaggedEvent(rawData) : Effect.succeed(void 0);
|
|
100
|
-
return Effect.flatMap(encodeData, (encodedData) => Effect.fail(invokeInterrupt({
|
|
108
|
+
return Effect.flatMap(encodeData, (encodedData) => Effect.andThen(flushIfCheckpoint, Effect.fail(invokeInterrupt({
|
|
101
109
|
info,
|
|
102
110
|
functionId: `${appName}-${options.function._tag}`,
|
|
103
111
|
payload: {
|
|
@@ -106,9 +114,12 @@ const createStepTools = (request, appName, stepIdCounts) => {
|
|
|
106
114
|
v: options.v ?? "1"
|
|
107
115
|
},
|
|
108
116
|
timeout: options.timeout ? timeStr(options.timeout) : "365d"
|
|
109
|
-
})));
|
|
117
|
+
}))));
|
|
110
118
|
}), Match.exhaustive));
|
|
111
|
-
const run = (opts, effect) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Step failed", {
|
|
119
|
+
const run = (opts, effect) => Effect.flatMap(getInfo(opts), (info) => pipe(getMemo(info.hash), Match.value, Match.tag("MemoData", ({ data }) => Effect.succeed(data)), Match.tag("MemoError", ({ error }) => stepError(info.id, Predicate.hasProperty(error, "message") ? String(error.message) : "Step failed", {
|
|
120
|
+
noRetry: true,
|
|
121
|
+
cause: error
|
|
122
|
+
})), Match.tag("MemoTimeout", () => stepError(info.id, "Step timed out", { noRetry: true })), Match.tag("MemoInput", () => stepError(info.id, "Unexpected step result type: input")), Match.tag("MemoNone", () => {
|
|
112
123
|
if (isBlocked(info.hash) || !canExecute(info.hash)) return Effect.fail(plannedInterrupt({ info }));
|
|
113
124
|
return effect.pipe(Effect.withSpan(`inngest.step/run/${info.id}`, { attributes: {
|
|
114
125
|
[OtelAttributes.StepId]: info.id,
|
|
@@ -117,18 +128,21 @@ const createStepTools = (request, appName, stepIdCounts) => {
|
|
|
117
128
|
onFailure: (err) => {
|
|
118
129
|
const noRetry = isNonRetriableError(err) ? true : void 0;
|
|
119
130
|
const retryAfterMs = isRetryAfterError(err) ? Duration.toMillis(err.retryAfter) : void 0;
|
|
120
|
-
return Effect.
|
|
131
|
+
return Effect.andThen(Effect.annotateCurrentSpan(errorOtelAttributes(err)), Effect.fail(errorInterrupt({
|
|
121
132
|
info,
|
|
122
133
|
error: err,
|
|
123
134
|
noRetry,
|
|
124
135
|
retryAfterMs
|
|
125
136
|
})));
|
|
126
137
|
},
|
|
127
|
-
onSuccess: (data) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
onSuccess: (data) => Option.match(checkpoint, {
|
|
139
|
+
onNone: () => Effect.fail(runInterrupt({
|
|
140
|
+
info,
|
|
141
|
+
data
|
|
142
|
+
})),
|
|
143
|
+
onSome: (state) => Effect.as(state.bufferStep(stepRun(info, data)), data)
|
|
144
|
+
})
|
|
145
|
+
}), Effect.catchDefect((defect) => Effect.andThen(Effect.annotateCurrentSpan(errorOtelAttributes(defect)), Effect.fail(errorInterrupt({
|
|
132
146
|
info,
|
|
133
147
|
error: defect
|
|
134
148
|
})))));
|
|
@@ -145,13 +159,16 @@ const createStepTools = (request, appName, stepIdCounts) => {
|
|
|
145
159
|
return Effect.flatMap(Effect.forEach(events, (e) => Effect.map(encodeTaggedEvent(e), (encoded) => ({
|
|
146
160
|
name: e._tag,
|
|
147
161
|
data: encoded
|
|
148
|
-
}))), (eventPayloads) =>
|
|
162
|
+
}))), (eventPayloads) => InngestClient.use((client) => client.sendEvent(eventPayloads).pipe(Effect.withSpan(`inngest.step/sendEvent/${info.id}`, { attributes: {
|
|
149
163
|
[OtelAttributes.StepId]: info.id,
|
|
150
164
|
[OtelAttributes.StepType]: "sendEvent"
|
|
151
|
-
} }), Effect.flatMap((result) =>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
} }), Effect.flatMap((result) => Option.match(checkpoint, {
|
|
166
|
+
onNone: () => Effect.fail(runInterrupt({
|
|
167
|
+
info,
|
|
168
|
+
data: result
|
|
169
|
+
})),
|
|
170
|
+
onSome: (state) => Effect.as(state.bufferStep(stepRun(info, result)), result)
|
|
171
|
+
})))));
|
|
155
172
|
}), Match.exhaustive));
|
|
156
173
|
return {
|
|
157
174
|
run,
|
|
@@ -187,6 +204,5 @@ const buildHandlerContext = (fn, step, request) => {
|
|
|
187
204
|
}
|
|
188
205
|
};
|
|
189
206
|
};
|
|
190
|
-
|
|
191
207
|
//#endregion
|
|
192
|
-
export {
|
|
208
|
+
export { buildHandlerContext, createStepTools };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-inngest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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,38 @@
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"scripts": {
|
|
94
|
-
"build": "
|
|
95
|
-
"
|
|
96
|
-
"
|
|
94
|
+
"build": "vp pack",
|
|
95
|
+
"test": "vp test run",
|
|
96
|
+
"test:watch": "vp test",
|
|
97
|
+
"format": "vp fmt",
|
|
98
|
+
"lint": "vp lint --fix --type-aware --type-check --promise-plugin",
|
|
97
99
|
"typecheck": "tsgo --noEmit",
|
|
98
|
-
"prepare": "effect-language-service patch",
|
|
100
|
+
"prepare": "vp config && effect-language-service patch && effect-tsgo patch",
|
|
99
101
|
"changeset": "changeset",
|
|
100
102
|
"changeset:version": "changeset version",
|
|
101
103
|
"changeset:publish": "bun run build && changeset publish"
|
|
102
104
|
},
|
|
103
105
|
"devDependencies": {
|
|
104
|
-
"@changesets/changelog-github": "^0.
|
|
105
|
-
"@changesets/cli": "^2.
|
|
106
|
-
"@effect/
|
|
107
|
-
"@effect/
|
|
108
|
-
"@effect/
|
|
109
|
-
"@
|
|
110
|
-
"@
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"oxfmt": "0.26.0",
|
|
114
|
-
"oxlint": "1.41.0",
|
|
115
|
-
"oxlint-tsgolint": "0.11.1",
|
|
116
|
-
"tsdown": "0.20.1",
|
|
117
|
-
"typescript": "^5.9.3"
|
|
106
|
+
"@changesets/changelog-github": "^0.6.0",
|
|
107
|
+
"@changesets/cli": "^2.31.0",
|
|
108
|
+
"@effect/platform-bun": "4.0.0-beta.59",
|
|
109
|
+
"@effect/tsgo": "0.5.2",
|
|
110
|
+
"@effect/vitest": "4.0.0-beta.59",
|
|
111
|
+
"@types/bun": "1.3.12",
|
|
112
|
+
"@typescript/native-preview": "^7.0.0-dev.20260420.1",
|
|
113
|
+
"typescript": "^5.9.3",
|
|
114
|
+
"vite-plus": "latest"
|
|
118
115
|
},
|
|
119
116
|
"peerDependencies": {
|
|
120
|
-
"
|
|
121
|
-
"
|
|
117
|
+
"effect": ">=4.0.0-beta.44",
|
|
118
|
+
"typescript": ">=5.9"
|
|
119
|
+
},
|
|
120
|
+
"overrides": {
|
|
121
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@latest",
|
|
122
|
+
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
123
|
+
},
|
|
124
|
+
"packageManager": "bun@1.3.13",
|
|
125
|
+
"patchedDependencies": {
|
|
126
|
+
"@effect/vitest@4.0.0-beta.59": "patches/@effect%2Fvitest@4.0.0-beta.59.patch"
|
|
122
127
|
}
|
|
123
128
|
}
|
package/src/Client.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
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 "
|
|
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 * as Clock from "effect/Clock";
|
|
7
8
|
import * as Config from "effect/Config";
|
|
8
|
-
import type * as ConfigError from "effect/ConfigError";
|
|
9
9
|
import * as Context from "effect/Context";
|
|
10
10
|
import * as Effect from "effect/Effect";
|
|
11
11
|
import * as Layer from "effect/Layer";
|
|
12
12
|
import * as Option from "effect/Option";
|
|
13
13
|
import * as Predicate from "effect/Predicate";
|
|
14
|
+
import * as Ref from "effect/Ref";
|
|
15
|
+
import * as Schedule from "effect/Schedule";
|
|
14
16
|
import * as Schema from "effect/Schema";
|
|
17
|
+
import { CheckpointApiError, type CheckpointingOption } from "./internal/checkpoint.js";
|
|
15
18
|
import * as Protocol from "./internal/protocol.js";
|
|
19
|
+
import { hashSigningKey } from "./internal/signature.js";
|
|
20
|
+
|
|
21
|
+
export type { CheckpointingOption } from "./internal/checkpoint.js";
|
|
22
|
+
export { CheckpointApiError } from "./internal/checkpoint.js";
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* @since 0.1.0
|
|
@@ -111,6 +118,23 @@ interface ClientConfig {
|
|
|
111
118
|
* The path where the Inngest serve handler is mounted. Used for registration.
|
|
112
119
|
*/
|
|
113
120
|
readonly servePath?: string | undefined;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Whether to use checkpointing by default for executions of functions
|
|
124
|
+
* created using this client.
|
|
125
|
+
*
|
|
126
|
+
* - `false` disables checkpointing for all functions on this client.
|
|
127
|
+
* - `true` enables checkpointing with safe defaults (`bufferedSteps: 1`,
|
|
128
|
+
* `maxInterval: 0`, `maxRuntime: 10s`). Steps are checkpointed to the
|
|
129
|
+
* Inngest API as they complete instead of yielding a 206 per step.
|
|
130
|
+
* - An object lets you tune `bufferedSteps`, `maxInterval`, `maxRuntime`.
|
|
131
|
+
*
|
|
132
|
+
* Per-function `checkpointing` overrides this setting. Defaults to `true`
|
|
133
|
+
* (checkpointing enabled).
|
|
134
|
+
*
|
|
135
|
+
* @default true
|
|
136
|
+
*/
|
|
137
|
+
readonly checkpointing?: CheckpointingOption | undefined;
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
export const EventPayload = Schema.Struct({
|
|
@@ -122,14 +146,14 @@ export const EventPayload = Schema.Struct({
|
|
|
122
146
|
});
|
|
123
147
|
|
|
124
148
|
const SendEventResponse = Schema.Struct({
|
|
125
|
-
ids: Schema.
|
|
149
|
+
ids: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
|
|
126
150
|
status: Schema.optional(Schema.Number),
|
|
127
151
|
});
|
|
128
152
|
|
|
129
153
|
type EventPayload = typeof EventPayload.Type;
|
|
130
154
|
type SendEventResponse = typeof SendEventResponse.Type;
|
|
131
155
|
|
|
132
|
-
class SendEventError extends Schema.
|
|
156
|
+
class SendEventError extends Schema.TaggedErrorClass<SendEventError>()("SendEventError", {
|
|
133
157
|
message: Schema.String,
|
|
134
158
|
events: Schema.Array(Schema.String),
|
|
135
159
|
}) {}
|
|
@@ -141,6 +165,23 @@ interface InngestClientService {
|
|
|
141
165
|
readonly eventBaseUrl: string;
|
|
142
166
|
readonly apiBaseUrl: string;
|
|
143
167
|
readonly sendEvent: (events: ReadonlyArray<EventPayload>) => Effect.Effect<SendEventResponse, SendEventError>;
|
|
168
|
+
/**
|
|
169
|
+
* Async checkpoint API call per spec §10.3.1.
|
|
170
|
+
*
|
|
171
|
+
* POSTs `{run_id, fn_id, qi_id, steps, ts}` to
|
|
172
|
+
* `{apiBaseUrl}/v1/checkpoint/{runId}/async` with a hashed-signing-key
|
|
173
|
+
* Bearer token. Retries 5xx responses with exponential backoff + jitter
|
|
174
|
+
* (5 attempts). On 401, falls back to the configured fallback signing key
|
|
175
|
+
* for the remainder of the client's lifetime (one-way switch).
|
|
176
|
+
*
|
|
177
|
+
* @internal
|
|
178
|
+
*/
|
|
179
|
+
readonly checkpointAsync: (args: {
|
|
180
|
+
readonly runId: string;
|
|
181
|
+
readonly fnId: string;
|
|
182
|
+
readonly qiId: string;
|
|
183
|
+
readonly steps: ReadonlyArray<typeof Protocol.GeneratorOpcode.Type>;
|
|
184
|
+
}) => Effect.Effect<void, CheckpointApiError>;
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
/**
|
|
@@ -149,7 +190,9 @@ 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
|
+
) {}
|
|
153
196
|
|
|
154
197
|
const resolveMode = (config: ClientConfig): ClientMode => config.mode ?? "dev";
|
|
155
198
|
|
|
@@ -159,64 +202,175 @@ const resolveEventBaseUrl = (config: ClientConfig, mode: ClientMode): string =>
|
|
|
159
202
|
const resolveApiBaseUrl = (config: ClientConfig, mode: ClientMode): string =>
|
|
160
203
|
config.apiBaseUrl ?? (mode === "dev" ? DEFAULT_DEV_SERVER_URL : DEFAULT_API_BASE_URL);
|
|
161
204
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const key = config.eventKey ?? "local";
|
|
176
|
-
const url = new URL(`e/${key}`, eventBaseUrl).toString();
|
|
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
|
-
);
|
|
205
|
+
const isRetriable = (error: CheckpointApiError): boolean => error.status === undefined || error.status >= 500;
|
|
206
|
+
|
|
207
|
+
const defaultCheckpointRetrySchedule: Schedule.Schedule<unknown, CheckpointApiError> = Schedule.exponential(
|
|
208
|
+
"100 millis",
|
|
209
|
+
2.0,
|
|
210
|
+
).pipe(
|
|
211
|
+
Schedule.jittered,
|
|
212
|
+
Schedule.both(Schedule.recurs(5)),
|
|
213
|
+
Schedule.while((m: Schedule.Metadata<unknown, CheckpointApiError>) => isRetriable(m.input)),
|
|
214
|
+
);
|
|
195
215
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
216
|
+
const makeClient = (
|
|
217
|
+
config: ClientConfig,
|
|
218
|
+
httpClient: HttpClient.HttpClient,
|
|
219
|
+
retrySchedule: Schedule.Schedule<unknown, CheckpointApiError> = defaultCheckpointRetrySchedule,
|
|
220
|
+
): Effect.Effect<InngestClientService> =>
|
|
221
|
+
Effect.gen(function* () {
|
|
222
|
+
const mode = resolveMode(config);
|
|
223
|
+
const eventBaseUrl = resolveEventBaseUrl(config, mode);
|
|
224
|
+
const apiBaseUrl = resolveApiBaseUrl(config, mode);
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* One-way switch: once the fallback signing key succeeds for any
|
|
228
|
+
* checkpoint API call, all subsequent calls use it first. Per spec
|
|
229
|
+
* §10.3.4.
|
|
230
|
+
*/
|
|
231
|
+
const useFallbackKey = yield* Ref.make(false);
|
|
232
|
+
|
|
233
|
+
const sendEvent = Effect.fn("InngestClient.sendEvent")(function* (events: ReadonlyArray<EventPayload>) {
|
|
234
|
+
if (!config.eventKey && mode === "cloud") {
|
|
235
|
+
return yield* Effect.fail(
|
|
203
236
|
new SendEventError({
|
|
204
|
-
message:
|
|
205
|
-
events:
|
|
237
|
+
message: "Event key is required to send events in cloud mode",
|
|
238
|
+
events: events.map((e) => e.name),
|
|
206
239
|
}),
|
|
207
|
-
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const key = config.eventKey ?? "local";
|
|
244
|
+
const url = new URL(`e/${key}`, eventBaseUrl).toString();
|
|
245
|
+
const eventNames = events.map((e) => e.name);
|
|
246
|
+
const now = yield* Clock.currentTimeMillis;
|
|
247
|
+
|
|
248
|
+
const payloads = events.map((e) => ({
|
|
249
|
+
name: e.name,
|
|
250
|
+
data: e.data ?? {},
|
|
251
|
+
ts: e.ts ?? now,
|
|
252
|
+
id: e.id,
|
|
253
|
+
v: e.v,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
const request = HttpClientRequest.post(url).pipe(
|
|
257
|
+
HttpClientRequest.setHeaders({
|
|
258
|
+
"Content-Type": "application/json",
|
|
259
|
+
[Protocol.Headers.SDK]: `effect-ts:v${SDK_VERSION}`,
|
|
260
|
+
...(config.env ? { [Protocol.Headers.Env]: config.env } : {}),
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return yield* HttpClientRequest.schemaBodyJson(Schema.Array(EventPayload))(request, payloads).pipe(
|
|
265
|
+
Effect.flatMap(httpClient.execute),
|
|
266
|
+
Effect.flatMap(HttpClientResponse.schemaBodyJson(SendEventResponse)),
|
|
267
|
+
Effect.map((response) => ({ ids: response.ids, status: response.status })),
|
|
268
|
+
Effect.scoped,
|
|
269
|
+
Effect.catch((error) =>
|
|
270
|
+
Effect.fail(
|
|
271
|
+
new SendEventError({
|
|
272
|
+
message: `Failed to send events: ${Predicate.hasProperty(error, "message") ? (error.message as string) : "Unknown error"}`,
|
|
273
|
+
events: eventNames,
|
|
274
|
+
}),
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Issue a checkpoint POST with the given signing key. Returns a tagged
|
|
282
|
+
* error on non-2xx so the caller can decide whether to retry, swap keys,
|
|
283
|
+
* or fall back.
|
|
284
|
+
*/
|
|
285
|
+
const checkpointRequest = (
|
|
286
|
+
runId: string,
|
|
287
|
+
body: string,
|
|
288
|
+
signingKey: string,
|
|
289
|
+
): Effect.Effect<void, CheckpointApiError> => {
|
|
290
|
+
const url = new URL(`v1/checkpoint/${runId}/async`, apiBaseUrl).toString();
|
|
291
|
+
const request = HttpClientRequest.post(url).pipe(
|
|
292
|
+
HttpClientRequest.setHeaders({
|
|
293
|
+
"Content-Type": "application/json",
|
|
294
|
+
[Protocol.Headers.SDK]: `effect-inngest:v${SDK_VERSION}`,
|
|
295
|
+
[Protocol.Headers.RequestVersion]: "2",
|
|
296
|
+
...(config.env ? { [Protocol.Headers.Env]: config.env } : {}),
|
|
297
|
+
}),
|
|
298
|
+
HttpClientRequest.bearerToken(hashSigningKey(signingKey)),
|
|
299
|
+
HttpClientRequest.bodyText(body, "application/json"),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return httpClient.execute(request).pipe(
|
|
303
|
+
Effect.scoped,
|
|
304
|
+
Effect.catch((error) =>
|
|
305
|
+
Effect.fail(
|
|
306
|
+
new CheckpointApiError({
|
|
307
|
+
message: `Network error issuing checkpoint: ${Predicate.hasProperty(error, "message") ? (error.message as string) : String(error)}`,
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
Effect.flatMap((response) => {
|
|
312
|
+
if (response.status >= 200 && response.status < 300) {
|
|
313
|
+
return Effect.void;
|
|
314
|
+
}
|
|
315
|
+
return Effect.fail(
|
|
316
|
+
new CheckpointApiError({
|
|
317
|
+
message: `Checkpoint API returned ${response.status}`,
|
|
318
|
+
status: response.status,
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const checkpointAsync: InngestClientService["checkpointAsync"] = Effect.fn("InngestClient.checkpointAsync")(
|
|
326
|
+
function* (args) {
|
|
327
|
+
const signingKey = config.signingKey;
|
|
328
|
+
if (!signingKey) {
|
|
329
|
+
return yield* Effect.fail(
|
|
330
|
+
new CheckpointApiError({ message: "No signing key configured for checkpoint API" }),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const now = yield* Clock.currentTimeMillis;
|
|
335
|
+
const body = JSON.stringify({
|
|
336
|
+
run_id: args.runId,
|
|
337
|
+
fn_id: args.fnId,
|
|
338
|
+
qi_id: args.qiId,
|
|
339
|
+
steps: Schema.encodeSync(Schema.Array(Protocol.GeneratorOpcode))(args.steps),
|
|
340
|
+
ts: now,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const fallbackKey = config.signingKeyFallback;
|
|
344
|
+
|
|
345
|
+
// Single attempt: tries the chosen key first; on 401 with primary,
|
|
346
|
+
// tries the fallback once and flips the Ref.
|
|
347
|
+
const attempt: Effect.Effect<void, CheckpointApiError> = Ref.get(useFallbackKey).pipe(
|
|
348
|
+
Effect.flatMap((usingFallback) => {
|
|
349
|
+
const primaryKey = usingFallback && fallbackKey ? fallbackKey : signingKey;
|
|
350
|
+
return checkpointRequest(args.runId, body, primaryKey).pipe(
|
|
351
|
+
Effect.catch((error) =>
|
|
352
|
+
error.status === 401 && !usingFallback && fallbackKey
|
|
353
|
+
? checkpointRequest(args.runId, body, fallbackKey).pipe(Effect.andThen(Ref.set(useFallbackKey, true)))
|
|
354
|
+
: Effect.fail(error),
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return yield* attempt.pipe(Effect.retry(retrySchedule));
|
|
361
|
+
},
|
|
208
362
|
);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
[TypeId]: TypeId,
|
|
366
|
+
config,
|
|
367
|
+
mode,
|
|
368
|
+
eventBaseUrl,
|
|
369
|
+
apiBaseUrl,
|
|
370
|
+
sendEvent,
|
|
371
|
+
checkpointAsync,
|
|
372
|
+
};
|
|
373
|
+
});
|
|
220
374
|
|
|
221
375
|
/**
|
|
222
376
|
* Create an InngestClient layer from a config.
|
|
@@ -234,7 +388,27 @@ const makeClient = (config: ClientConfig, httpClient: HttpClient.HttpClient): In
|
|
|
234
388
|
export const layer = (config: ClientConfig): Layer.Layer<InngestClient, never, HttpClient.HttpClient> =>
|
|
235
389
|
Layer.effect(
|
|
236
390
|
InngestClient,
|
|
237
|
-
Effect.
|
|
391
|
+
Effect.gen(function* () {
|
|
392
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
393
|
+
return yield* makeClient(config, httpClient);
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Test-only factory attached via Symbol.for; not a named export, not in
|
|
398
|
+
// TypeScript autocomplete, not in the public `InngestClient.*` namespace.
|
|
399
|
+
// Tests retrieve it by re-deriving the same global symbol. Do not use from
|
|
400
|
+
// application code.
|
|
401
|
+
const TEST_FACTORY_KEY = Symbol.for("effect-inngest/internal/test-retry-schedule-factory");
|
|
402
|
+
(InngestClient as unknown as Record<symbol, unknown>)[TEST_FACTORY_KEY] = (
|
|
403
|
+
config: ClientConfig,
|
|
404
|
+
retrySchedule: Schedule.Schedule<unknown, CheckpointApiError>,
|
|
405
|
+
): Layer.Layer<InngestClient, never, HttpClient.HttpClient> =>
|
|
406
|
+
Layer.effect(
|
|
407
|
+
InngestClient,
|
|
408
|
+
Effect.gen(function* () {
|
|
409
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
410
|
+
return yield* makeClient(config, httpClient, retrySchedule);
|
|
411
|
+
}),
|
|
238
412
|
);
|
|
239
413
|
|
|
240
414
|
/**
|
|
@@ -244,13 +418,15 @@ export const layer = (config: ClientConfig): Layer.Layer<InngestClient, never, H
|
|
|
244
418
|
* @category layers
|
|
245
419
|
*/
|
|
246
420
|
export const layerConfig = (
|
|
247
|
-
config: Config.
|
|
248
|
-
): Layer.Layer<InngestClient,
|
|
421
|
+
config: Config.Wrap<ClientConfig>,
|
|
422
|
+
): Layer.Layer<InngestClient, Config.ConfigError, HttpClient.HttpClient> =>
|
|
249
423
|
Layer.effect(
|
|
250
424
|
InngestClient,
|
|
251
|
-
Effect.
|
|
252
|
-
|
|
253
|
-
|
|
425
|
+
Effect.gen(function* () {
|
|
426
|
+
const resolvedConfig = yield* Config.unwrap(config);
|
|
427
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
428
|
+
return yield* makeClient(resolvedConfig, httpClient);
|
|
429
|
+
}),
|
|
254
430
|
);
|
|
255
431
|
|
|
256
432
|
/**
|
|
@@ -266,7 +442,7 @@ export const layerConfig = (
|
|
|
266
442
|
* @since 0.1.0
|
|
267
443
|
* @category layers
|
|
268
444
|
*/
|
|
269
|
-
export const layerFromEnv: Layer.Layer<InngestClient,
|
|
445
|
+
export const layerFromEnv: Layer.Layer<InngestClient, Config.ConfigError, HttpClient.HttpClient> = layerConfig(
|
|
270
446
|
Config.all({
|
|
271
447
|
id: Config.string("INNGEST_APP_ID").pipe(Config.withDefault("app")),
|
|
272
448
|
eventKey: Config.string("INNGEST_EVENT_KEY").pipe(Config.option, Config.map(Option.getOrUndefined)),
|
package/src/Events.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class FunctionFailed extends Schema.TaggedClass<FunctionFailed>()("innges
|
|
|
25
25
|
function_id: Schema.String,
|
|
26
26
|
run_id: Schema.String,
|
|
27
27
|
error: JsonError,
|
|
28
|
-
event: Schema.Record(
|
|
28
|
+
event: Schema.Record(Schema.String, Schema.Unknown),
|
|
29
29
|
}) {}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -57,7 +57,7 @@ export class FunctionFinishedSuccess extends Schema.TaggedClass<FunctionFinished
|
|
|
57
57
|
* Union of both FunctionFinished variants.
|
|
58
58
|
* @since 0.1.0
|
|
59
59
|
*/
|
|
60
|
-
export const FunctionFinished = Schema.Union(FunctionFinishedError, FunctionFinishedSuccess);
|
|
60
|
+
export const FunctionFinished = Schema.Union([FunctionFinishedError, FunctionFinishedSuccess]);
|
|
61
61
|
export type FunctionFinished = typeof FunctionFinished.Type;
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -75,7 +75,7 @@ export class FunctionCancelled extends Schema.TaggedClass<FunctionCancelled>()("
|
|
|
75
75
|
* @since 0.1.0
|
|
76
76
|
*/
|
|
77
77
|
export class FunctionInvoked extends Schema.TaggedClass<FunctionInvoked>()("inngest/function.invoked", {
|
|
78
|
-
data: Schema.optional(Schema.Record(
|
|
78
|
+
data: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
79
79
|
}) {}
|
|
80
80
|
|
|
81
81
|
/**
|