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/handler.ts
CHANGED
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
* Internal handler implementation.
|
|
3
3
|
* @internal
|
|
4
4
|
*/
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
8
|
-
import * as
|
|
5
|
+
import * as Headers from "effect/unstable/http/Headers";
|
|
6
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
7
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
8
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
9
|
+
import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
|
|
10
|
+
import * as Cause from "effect/Cause";
|
|
9
11
|
import * as Context from "effect/Context";
|
|
10
12
|
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
11
14
|
import * as Predicate from "effect/Predicate";
|
|
12
15
|
import * as Schema from "effect/Schema";
|
|
13
16
|
import { InngestClient } from "../Client.js";
|
|
14
17
|
import type { InngestFunction } from "../Function.js";
|
|
15
18
|
import type { InngestGroup } from "../Group.js";
|
|
19
|
+
import * as Checkpoint from "./checkpoint.js";
|
|
16
20
|
import { Signature, SignatureError } from "./signature.js";
|
|
17
21
|
import * as Protocol from "./protocol.js";
|
|
18
22
|
export { SignatureError } from "./signature.js";
|
|
19
23
|
import { execute, type TraceHeaders } from "./driver.js";
|
|
20
24
|
export type { TraceHeaders } from "./driver.js";
|
|
21
25
|
|
|
22
|
-
export class InvalidRequestError extends Schema.
|
|
26
|
+
export class InvalidRequestError extends Schema.TaggedErrorClass<InvalidRequestError>()("InvalidRequestError", {
|
|
23
27
|
message: Schema.String,
|
|
24
28
|
}) {}
|
|
25
29
|
|
|
@@ -28,7 +32,7 @@ const SDK_VERSION = "2.0.0";
|
|
|
28
32
|
const baseHeaders = (): Record<string, string> => ({
|
|
29
33
|
"Content-Type": "application/json",
|
|
30
34
|
[Protocol.Headers.SDK]: `effect-inngest:v${SDK_VERSION}`,
|
|
31
|
-
[Protocol.Headers.RequestVersion]: "
|
|
35
|
+
[Protocol.Headers.RequestVersion]: "2",
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
const buildServeUrl = (requestUrl: string, serveHost?: string, servePath?: string): URL => {
|
|
@@ -48,119 +52,140 @@ export interface HandlerResponse<T> {
|
|
|
48
52
|
readonly body: T;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
export const verifyAndParseRequestBody = (
|
|
55
|
+
export const verifyAndParseRequestBody = Effect.fn("effect-inngest/handler/verifyAndParseRequestBody")(function* (
|
|
52
56
|
request: HttpServerRequest.HttpServerRequest,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Effect.gen(function* () {
|
|
59
|
-
const client = yield* InngestClient;
|
|
60
|
-
const sig = yield* Signature;
|
|
61
|
-
const config = client.config;
|
|
62
|
-
const isDev = client.mode === "dev";
|
|
63
|
-
|
|
64
|
-
const bodyText = yield* request.text.pipe(
|
|
65
|
-
Effect.mapError((error) => new InvalidRequestError({ message: `Failed to read request body: ${String(error)}` })),
|
|
66
|
-
);
|
|
57
|
+
) {
|
|
58
|
+
const client = yield* InngestClient;
|
|
59
|
+
const sig = yield* Signature;
|
|
60
|
+
const config = client.config;
|
|
61
|
+
const isDev = client.mode === "dev";
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
63
|
+
const bodyText = yield* request.text.pipe(
|
|
64
|
+
Effect.mapError((error) => {
|
|
65
|
+
const msg =
|
|
66
|
+
Predicate.hasProperty(error, "message") && typeof error.message === "string" ? error.message : "unknown";
|
|
67
|
+
return new InvalidRequestError({ message: `Failed to read request body: ${msg}` });
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
71
|
+
yield* sig.verify({
|
|
72
|
+
body: new TextEncoder().encode(bodyText),
|
|
73
|
+
signatureHeader: Option.getOrUndefined(Headers.get(request.headers, Protocol.Headers.Signature)),
|
|
74
|
+
signingKey: config.signingKey,
|
|
75
|
+
signingKeyFallback: config.signingKeyFallback,
|
|
76
|
+
isDev,
|
|
79
77
|
});
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Protocol.SDKRequestBody))(bodyText).pipe(
|
|
80
|
+
Effect.mapError((error) => new InvalidRequestError({ message: `Invalid request body: ${String(error)}` })),
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const handleIntrospection = Effect.fn("effect-inngest/handler/handleIntrospection")(function* (
|
|
82
85
|
group: InngestGroup.Any,
|
|
83
86
|
_requestUrl: string,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const config = client.config;
|
|
88
|
-
|
|
89
|
-
const body: typeof Protocol.IntrospectionResponse.Type = {
|
|
90
|
-
function_count: group.functions.size,
|
|
91
|
-
has_event_key: config.eventKey !== undefined,
|
|
92
|
-
has_signing_key: config.signingKey !== undefined,
|
|
93
|
-
has_signing_key_fallback: config.signingKeyFallback !== undefined,
|
|
94
|
-
mode: client.mode === "dev" ? "dev" : "cloud",
|
|
95
|
-
schema_version: "2024-05-24",
|
|
96
|
-
authentication_succeeded: null,
|
|
97
|
-
};
|
|
87
|
+
) {
|
|
88
|
+
const client = yield* InngestClient;
|
|
89
|
+
const config = client.config;
|
|
98
90
|
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
const body: typeof Protocol.IntrospectionResponse.Type = {
|
|
92
|
+
function_count: group.functions.size,
|
|
93
|
+
has_event_key: config.eventKey !== undefined,
|
|
94
|
+
has_signing_key: config.signingKey !== undefined,
|
|
95
|
+
has_signing_key_fallback: config.signingKeyFallback !== undefined,
|
|
96
|
+
mode: client.mode === "dev" ? "dev" : "cloud",
|
|
97
|
+
schema_version: "2024-05-24",
|
|
98
|
+
authentication_succeeded: null,
|
|
99
|
+
};
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
url: Schema.String,
|
|
104
|
-
v: Schema.String,
|
|
105
|
-
deployType: Schema.Literal("ping"),
|
|
106
|
-
sdk: Schema.String,
|
|
107
|
-
appName: Schema.String,
|
|
108
|
-
framework: Schema.String,
|
|
109
|
-
functions: Schema.Array(Schema.Unknown),
|
|
101
|
+
return { status: 200, headers: baseHeaders(), body } as HandlerResponse<typeof Protocol.IntrospectionResponse.Type>;
|
|
110
102
|
});
|
|
111
103
|
|
|
112
|
-
export const handleRegistration = (
|
|
104
|
+
export const handleRegistration = Effect.fn("effect-inngest/handler/handleRegistration")(function* (
|
|
113
105
|
group: InngestGroup.Any,
|
|
114
106
|
requestUrl: string,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Effect.gen(function* () {
|
|
121
|
-
const client = yield* InngestClient;
|
|
122
|
-
const httpClient = yield* HttpClient.HttpClient;
|
|
123
|
-
const config = client.config;
|
|
124
|
-
const url = buildServeUrl(requestUrl, config.serveHost, config.servePath);
|
|
125
|
-
|
|
126
|
-
const functions = Array.from(group.functions.values()).map((fn) =>
|
|
127
|
-
fn.toRegistration({ appId: config.id, url: url.href }),
|
|
128
|
-
);
|
|
107
|
+
) {
|
|
108
|
+
const client = yield* InngestClient;
|
|
109
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
110
|
+
const config = client.config;
|
|
111
|
+
const url = buildServeUrl(requestUrl, config.serveHost, config.servePath);
|
|
129
112
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
113
|
+
const functions = Array.from(group.functions.values()).map((fn) =>
|
|
114
|
+
fn.toRegistration({ appId: config.id, url: url.href }),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const registerUrl = new URL("fn/register", client.apiBaseUrl).toString();
|
|
118
|
+
const request = HttpClientRequest.post(registerUrl).pipe(
|
|
119
|
+
HttpClientRequest.setHeaders({
|
|
120
|
+
...baseHeaders(),
|
|
121
|
+
Authorization: `Bearer ${config.signingKey ?? ""}`,
|
|
122
|
+
}),
|
|
123
|
+
HttpClientRequest.bodyJsonUnsafe({
|
|
124
|
+
url: url.href,
|
|
125
|
+
v: "0.1",
|
|
126
|
+
deployType: "ping" as const,
|
|
127
|
+
sdk: `effect-inngest:v${SDK_VERSION}`,
|
|
128
|
+
appName: config.id,
|
|
129
|
+
framework: "effect",
|
|
130
|
+
functions,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
146
133
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}),
|
|
155
|
-
),
|
|
134
|
+
// Spec §4.3.1: PUT sync responds with `{ message: string; modified: boolean }`.
|
|
135
|
+
// Spec §4.3.3 / §4.3.4: 500 on failure with `modified: false`; 200 on success
|
|
136
|
+
// with the executor-supplied `modified` value (or `false` if omitted).
|
|
137
|
+
return yield* Effect.gen(function* () {
|
|
138
|
+
const response = yield* httpClient.execute(request).pipe(Effect.scoped);
|
|
139
|
+
const responseBody = yield* HttpClientResponse.schemaBodyJson(Protocol.RegisterServerResponse)(response).pipe(
|
|
140
|
+
Effect.catch(() => Effect.succeed({ error: "Unknown registration response" } as const)),
|
|
156
141
|
);
|
|
157
142
|
|
|
143
|
+
if (response.status !== 200 || !Predicate.hasProperty(responseBody, "ok")) {
|
|
144
|
+
return {
|
|
145
|
+
status: 500,
|
|
146
|
+
headers: baseHeaders(),
|
|
147
|
+
body: {
|
|
148
|
+
message:
|
|
149
|
+
Predicate.hasProperty(responseBody, "error") && responseBody.error
|
|
150
|
+
? responseBody.error
|
|
151
|
+
: `Registration failed with status ${response.status}`,
|
|
152
|
+
modified: false,
|
|
153
|
+
},
|
|
154
|
+
} as HandlerResponse<typeof Protocol.RegisterResponse.Type>;
|
|
155
|
+
}
|
|
156
|
+
|
|
158
157
|
return {
|
|
159
158
|
status: 200,
|
|
160
159
|
headers: baseHeaders(),
|
|
161
|
-
body:
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
body: {
|
|
161
|
+
message: "Successfully synced.",
|
|
162
|
+
modified: responseBody.modified ?? false,
|
|
163
|
+
},
|
|
164
|
+
} as HandlerResponse<typeof Protocol.RegisterResponse.Type>;
|
|
165
|
+
}).pipe(
|
|
166
|
+
Effect.catchCause((cause) => {
|
|
167
|
+
// Network/transport failure (e.g. die from HttpClient mock). Per
|
|
168
|
+
// §4.3.3 SHOULD return 500 with the body shape; never let the
|
|
169
|
+
// toWebHandler default 500 wrapper escape.
|
|
170
|
+
const errorOpt = Cause.findErrorOption(cause);
|
|
171
|
+
const dieReason = cause.reasons.find(Cause.isDieReason);
|
|
172
|
+
const message = Option.isSome(errorOpt)
|
|
173
|
+
? Predicate.hasProperty(errorOpt.value, "message") && typeof errorOpt.value.message === "string"
|
|
174
|
+
? errorOpt.value.message
|
|
175
|
+
: "Registration failed"
|
|
176
|
+
: dieReason
|
|
177
|
+
? Predicate.hasProperty(dieReason.defect, "message") && typeof dieReason.defect.message === "string"
|
|
178
|
+
? dieReason.defect.message
|
|
179
|
+
: "Registration failed"
|
|
180
|
+
: "Registration failed";
|
|
181
|
+
return Effect.succeed({
|
|
182
|
+
status: 500,
|
|
183
|
+
headers: baseHeaders(),
|
|
184
|
+
body: { message, modified: false },
|
|
185
|
+
} as HandlerResponse<typeof Protocol.RegisterResponse.Type>);
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
164
189
|
|
|
165
190
|
export interface Handler<Tag extends string> {
|
|
166
191
|
readonly tag: Tag;
|
|
@@ -168,55 +193,73 @@ export interface Handler<Tag extends string> {
|
|
|
168
193
|
readonly context: Context.Context<any>;
|
|
169
194
|
}
|
|
170
195
|
|
|
171
|
-
export const handleExecution = (
|
|
196
|
+
export const handleExecution = Effect.fn("effect-inngest/handler/handleExecution")(function* (
|
|
172
197
|
group: InngestGroup.Any,
|
|
173
198
|
fnId: string,
|
|
174
199
|
urlStepId: string | undefined,
|
|
175
200
|
body: typeof Protocol.SDKRequestBody.Type,
|
|
176
201
|
traceHeaders: TraceHeaders = {},
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const context = yield* Effect.context<never>();
|
|
202
|
+
) {
|
|
203
|
+
const client = yield* InngestClient;
|
|
204
|
+
const context = yield* Effect.context<never>();
|
|
181
205
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
206
|
+
const appId = client.config.id;
|
|
207
|
+
const prefix = `${appId}-`;
|
|
208
|
+
const fnTag = fnId.startsWith(prefix) ? fnId.slice(prefix.length) : fnId;
|
|
185
209
|
|
|
186
|
-
|
|
187
|
-
|
|
210
|
+
const fn = group.functions.get(fnTag) as InngestFunction.Any | undefined;
|
|
211
|
+
const entry = fn ? (context.mapUnsafe.get(fn.key) as Handler<string> | undefined) : undefined;
|
|
188
212
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
}
|
|
213
|
+
if (!fn || !entry) {
|
|
214
|
+
// Spec §4.4.1: unknown function MUST return 500 with the error payload
|
|
215
|
+
// at the response root (no { error: ... } envelope).
|
|
216
|
+
// Spec §4.4.3 pair rule: 500 MUST pair with X-Inngest-No-Retry: false.
|
|
217
|
+
return {
|
|
218
|
+
status: 500 as const,
|
|
219
|
+
headers: { ...baseHeaders(), [Protocol.Headers.NoRetry]: "false" },
|
|
220
|
+
body: Protocol.UserError.make({ name: "FunctionNotFoundError", message: `Unknown function: ${fnId}` }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
198
223
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
use_api: body.use_api,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
// URL stepId takes precedence over body.ctx.step_id
|
|
225
|
+
// This is how Inngest requests execution of a specific step
|
|
226
|
+
const effectiveBody =
|
|
227
|
+
urlStepId && urlStepId !== body.ctx.step_id
|
|
228
|
+
? Protocol.SDKRequestBody.make({
|
|
229
|
+
event: body.event,
|
|
230
|
+
events: body.events,
|
|
231
|
+
steps: body.steps,
|
|
232
|
+
ctx: Protocol.SDKRequestContext.make({
|
|
233
|
+
fn_id: body.ctx.fn_id,
|
|
234
|
+
run_id: body.ctx.run_id,
|
|
235
|
+
env: body.ctx.env,
|
|
236
|
+
step_id: urlStepId,
|
|
237
|
+
attempt: body.ctx.attempt,
|
|
238
|
+
max_attempts: body.ctx.max_attempts,
|
|
239
|
+
qi_id: body.ctx.qi_id,
|
|
240
|
+
disable_immediate_execution: body.ctx.disable_immediate_execution,
|
|
241
|
+
use_api: body.ctx.use_api,
|
|
242
|
+
stack: body.ctx.stack,
|
|
243
|
+
}),
|
|
244
|
+
version: body.version,
|
|
245
|
+
use_api: body.use_api,
|
|
246
|
+
})
|
|
247
|
+
: body;
|
|
248
|
+
|
|
249
|
+
// Checkpoint mode entry decision (spec §10):
|
|
250
|
+
// - URL stepId NOT set (we are not running a targeted step)
|
|
251
|
+
// - ctx.fn_id present (executor knows the function)
|
|
252
|
+
// - ctx.disable_immediate_execution false (executor allowed it)
|
|
253
|
+
// - resolved per-fn or per-client config not opted out
|
|
254
|
+
const enterCheckpoint =
|
|
255
|
+
!urlStepId && effectiveBody.ctx.fn_id !== "" && !effectiveBody.ctx.disable_immediate_execution;
|
|
256
|
+
const checkpointConfig = enterCheckpoint
|
|
257
|
+
? Option.fromNullishOr(Checkpoint.resolveConfig(fn.options.checkpointing, client.config.checkpointing))
|
|
258
|
+
: Option.none<Checkpoint.CheckpointConfig>();
|
|
259
|
+
|
|
260
|
+
const result = yield* Effect.provide(
|
|
261
|
+
execute(fn, entry.handler, effectiveBody, appId, traceHeaders, checkpointConfig),
|
|
262
|
+
entry.context,
|
|
263
|
+
);
|
|
264
|
+
return { status: result.status, headers: result.headers, body: result.body } as HandlerResponse<unknown>;
|
|
265
|
+
});
|
package/src/internal/helpers.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Internal helper utilities.
|
|
3
3
|
* @internal
|
|
4
4
|
*/
|
|
5
|
+
import * as DateTime from "effect/DateTime";
|
|
5
6
|
import * as Duration from "effect/Duration";
|
|
6
7
|
|
|
7
8
|
const millisecond = 1;
|
|
@@ -17,6 +18,7 @@ const periods = [
|
|
|
17
18
|
["h", hour],
|
|
18
19
|
["m", minute],
|
|
19
20
|
["s", second],
|
|
21
|
+
["ms", millisecond],
|
|
20
22
|
] as const;
|
|
21
23
|
|
|
22
24
|
/**
|
|
@@ -25,8 +27,8 @@ const periods = [
|
|
|
25
27
|
* Supports weeks, days, hours, minutes, and seconds. Years/months are converted
|
|
26
28
|
* to their equivalent in weeks/days.
|
|
27
29
|
*/
|
|
28
|
-
export const timeStr = (input: Duration.
|
|
29
|
-
let ms = Duration.toMillis(Duration.
|
|
30
|
+
export const timeStr = (input: Duration.Input): string => {
|
|
31
|
+
let ms = Duration.toMillis(Duration.fromInputUnsafe(input));
|
|
30
32
|
|
|
31
33
|
const [, result] = periods.reduce<[number, string]>(
|
|
32
34
|
([num, str], [suffix, period]) => {
|
|
@@ -46,13 +48,11 @@ export const timeStr = (input: Duration.DurationInput): string => {
|
|
|
46
48
|
|
|
47
49
|
/**
|
|
48
50
|
* Format a timestamp as an ISO string for Inngest's sleepUntil.
|
|
51
|
+
*
|
|
52
|
+
* String inputs are passed through unchanged (assumed ISO-formatted), matching
|
|
53
|
+
* the prior contract. Date/number inputs are normalized via DateTime.
|
|
49
54
|
*/
|
|
50
55
|
export const formatTimestamp = (timestamp: Date | number | string): string => {
|
|
51
|
-
if (timestamp
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
if (typeof timestamp === "number") {
|
|
55
|
-
return new Date(timestamp).toISOString();
|
|
56
|
-
}
|
|
57
|
-
return timestamp;
|
|
56
|
+
if (typeof timestamp === "string") return timestamp;
|
|
57
|
+
return DateTime.formatIso(DateTime.makeUnsafe(timestamp));
|
|
58
58
|
};
|
package/src/internal/memo.ts
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* Step memoization schemas for decoding cached step results.
|
|
3
3
|
* @internal
|
|
4
4
|
*/
|
|
5
|
+
import * as Option from "effect/Option";
|
|
5
6
|
import * as Predicate from "effect/Predicate";
|
|
6
7
|
import * as Schema from "effect/Schema";
|
|
8
|
+
import * as SchemaTransformation from "effect/SchemaTransformation";
|
|
7
9
|
|
|
8
10
|
// Wire format: require property KEY to exist (not just value to be defined)
|
|
9
11
|
const hasKey =
|
|
10
12
|
(key: string) =>
|
|
11
13
|
(u: unknown): u is Record<string, unknown> =>
|
|
12
|
-
Predicate.
|
|
14
|
+
Predicate.isObject(u) && Predicate.hasProperty(u, key);
|
|
13
15
|
|
|
14
16
|
// Output schemas (tagged structs)
|
|
15
17
|
const MemoDataSchema = Schema.TaggedStruct("MemoData", { data: Schema.Unknown });
|
|
@@ -20,41 +22,60 @@ const MemoNoneSchema = Schema.TaggedStruct("MemoNone", {});
|
|
|
20
22
|
|
|
21
23
|
// Wire schemas with property existence filters + tag attachment
|
|
22
24
|
const DataWire = Schema.Unknown.pipe(
|
|
23
|
-
Schema.
|
|
24
|
-
Schema.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
Schema.check(Schema.makeFilter(hasKey("data"))),
|
|
26
|
+
Schema.decodeTo(
|
|
27
|
+
MemoDataSchema,
|
|
28
|
+
SchemaTransformation.transform({
|
|
29
|
+
decode: (v) => ({ _tag: "MemoData" as const, data: (v as Record<string, unknown>).data }),
|
|
30
|
+
encode: ({ data }) => ({ data }),
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
28
33
|
);
|
|
29
34
|
|
|
30
35
|
const ErrorWire = Schema.Unknown.pipe(
|
|
31
|
-
Schema.
|
|
32
|
-
Schema.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
Schema.check(Schema.makeFilter(hasKey("error"))),
|
|
37
|
+
Schema.decodeTo(
|
|
38
|
+
MemoErrorSchema,
|
|
39
|
+
SchemaTransformation.transform({
|
|
40
|
+
decode: (v) => ({ _tag: "MemoError" as const, error: (v as Record<string, unknown>).error }),
|
|
41
|
+
encode: ({ error }) => ({ error }),
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
36
44
|
);
|
|
37
45
|
|
|
38
46
|
const InputWire = Schema.Unknown.pipe(
|
|
39
|
-
Schema.
|
|
40
|
-
Schema.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
Schema.check(Schema.makeFilter(hasKey("input"))),
|
|
48
|
+
Schema.decodeTo(
|
|
49
|
+
MemoInputSchema,
|
|
50
|
+
SchemaTransformation.transform({
|
|
51
|
+
decode: (v) => ({ _tag: "MemoInput" as const, input: (v as Record<string, unknown>).input }),
|
|
52
|
+
encode: ({ input }) => ({ input }),
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
44
55
|
);
|
|
45
56
|
|
|
46
|
-
const TimeoutWire = Schema.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
const TimeoutWire = Schema.Null.pipe(
|
|
58
|
+
Schema.decodeTo(
|
|
59
|
+
MemoTimeoutSchema,
|
|
60
|
+
SchemaTransformation.transform({
|
|
61
|
+
decode: () => ({ _tag: "MemoTimeout" as const }),
|
|
62
|
+
encode: () => null,
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
50
66
|
|
|
51
|
-
const NoneWire = Schema.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
const NoneWire = Schema.Undefined.pipe(
|
|
68
|
+
Schema.decodeTo(
|
|
69
|
+
MemoNoneSchema,
|
|
70
|
+
SchemaTransformation.transform({
|
|
71
|
+
decode: () => ({ _tag: "MemoNone" as const }),
|
|
72
|
+
encode: () => undefined,
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
55
76
|
|
|
56
77
|
// Union order matters: error > input > data (more specific first)
|
|
57
|
-
const MemoSchema = Schema.Union(ErrorWire, InputWire, DataWire, TimeoutWire, NoneWire);
|
|
78
|
+
const MemoSchema = Schema.Union([ErrorWire, InputWire, DataWire, TimeoutWire, NoneWire]);
|
|
58
79
|
|
|
59
80
|
// Derived type (only union is needed externally - Match.tag uses string literals)
|
|
60
81
|
export type Memo = Schema.Schema.Type<typeof MemoSchema>;
|
|
@@ -63,11 +84,5 @@ export type Memo = Schema.Schema.Type<typeof MemoSchema>;
|
|
|
63
84
|
* Decode a step result into a Memo type.
|
|
64
85
|
* Order matters: error > input > data (more specific properties first).
|
|
65
86
|
*/
|
|
66
|
-
export const decodeMemo = (value: unknown): Memo =>
|
|
67
|
-
|
|
68
|
-
if (result._tag === "Some") {
|
|
69
|
-
return result.value;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return MemoNoneSchema.make();
|
|
73
|
-
};
|
|
87
|
+
export const decodeMemo = (value: unknown): Memo =>
|
|
88
|
+
Option.getOrElse(Schema.decodeUnknownOption(MemoSchema)(value), () => MemoNoneSchema.make({}));
|