effect-cf 0.18.0 → 0.20.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/CHANGELOG.md +18 -0
- package/README.md +16 -2
- package/dist/{Hyperdrive-C0vfrPeG.d.mts → Hyperdrive-RTjgirMV.d.mts} +7 -12
- package/dist/HyperdrivePg.d.mts +2 -6
- package/dist/HyperdrivePg.mjs +0 -1
- package/dist/index.d.mts +160 -106
- package/dist/index.mjs +550 -168
- package/package.json +8 -9
- package/src/AiGateway.ts +3 -0
- package/src/AnalyticsEngine.ts +6 -1
- package/src/Binding.ts +4 -2
- package/src/CloudflareOtlp.ts +20 -7
- package/src/ContainerNamespace.ts +3 -0
- package/src/D1.ts +1 -0
- package/src/DurableObject.ts +10 -2
- package/src/DurableObjectAlarm.ts +19 -0
- package/src/DurableObjectDefinition.ts +34 -6
- package/src/DurableObjectNamespace.ts +4 -1
- package/src/DurableObjectRpcWebSocket.ts +14 -0
- package/src/DurableObjectSqlite.ts +1 -0
- package/src/DurableObjectStorage.ts +2 -0
- package/src/DurableObjectWebSocket.ts +4 -1
- package/src/Email.ts +354 -10
- package/src/HyperdrivePg.ts +1 -1
- package/src/Kv.ts +3 -1
- package/src/QueueBinding.ts +2 -1
- package/src/QueueDefinition.ts +6 -1
- package/src/R2.ts +1 -0
- package/src/RpcDefinition.ts +2 -0
- package/src/ServiceBinding.ts +6 -1
- package/src/Worker.ts +10 -2
- package/src/WorkerDefinition.ts +28 -6
- package/src/WorkflowBinding.ts +5 -2
- package/src/WorkflowDefinition.ts +6 -1
- package/src/internal/Clock.ts +9 -0
- package/src/internal/RpcInvocation.ts +1 -0
- package/dist/Hyperdrive-89hg_Fck.mjs +0 -156
- /package/dist/{chunk-CfYAbeIz.mjs → rolldown-runtime-D7D4PA-g.mjs} +0 -0
|
@@ -46,6 +46,7 @@ export const fromWebSocket = <Attachment = unknown>(
|
|
|
46
46
|
raw: WebSocket,
|
|
47
47
|
): DurableWebSocket<Attachment> => {
|
|
48
48
|
const existing = wrappers.get(raw);
|
|
49
|
+
|
|
49
50
|
if (existing !== undefined) {
|
|
50
51
|
return existing as DurableWebSocket<Attachment>;
|
|
51
52
|
}
|
|
@@ -74,6 +75,7 @@ export const fromWebSocket = <Attachment = unknown>(
|
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
wrappers.set(raw, socket);
|
|
78
|
+
|
|
77
79
|
return socket as DurableWebSocket<Attachment>;
|
|
78
80
|
};
|
|
79
81
|
|
|
@@ -189,7 +191,8 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
|
|
|
189
191
|
const deserialize = (socket: DurableWebSocket<unknown>) =>
|
|
190
192
|
Effect.gen(function* () {
|
|
191
193
|
const value = yield* socket.deserializeAttachment;
|
|
192
|
-
|
|
194
|
+
|
|
195
|
+
if (value === null || value === undefined) {
|
|
193
196
|
return Option.none<Attachment>();
|
|
194
197
|
}
|
|
195
198
|
|
package/src/Email.ts
CHANGED
|
@@ -11,12 +11,71 @@ import * as Binding from "./Binding";
|
|
|
11
11
|
import type { WorkerEnvironment } from "./Environment";
|
|
12
12
|
|
|
13
13
|
const expectedSendEmailBinding = "Send Email binding with send()";
|
|
14
|
+
const textEncoder = new TextEncoder();
|
|
15
|
+
|
|
16
|
+
/** Documented Cloudflare Email Sending limits. */
|
|
17
|
+
export const sendLimits = {
|
|
18
|
+
/** Combined `to`, `cc`, and `bcc` addresses per message. */
|
|
19
|
+
maxRecipients: 50,
|
|
20
|
+
maxAttachments: 32,
|
|
21
|
+
/** Total message size, including attachments. Not enforced client side. */
|
|
22
|
+
maxMessageBytes: 5 * 1024 * 1024,
|
|
23
|
+
maxHeaderNameBytes: 100,
|
|
24
|
+
maxHeaderValueBytes: 2048,
|
|
25
|
+
maxHeadersBytes: 16 * 1024,
|
|
26
|
+
/** Allowlisted custom headers, excluding `X-` prefixed headers. */
|
|
27
|
+
maxAllowlistHeaders: 20,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
/** Error codes reported by Cloudflare Email Sending. */
|
|
31
|
+
export const emailErrorCodes = [
|
|
32
|
+
"E_VALIDATION_ERROR",
|
|
33
|
+
"E_FIELD_MISSING",
|
|
34
|
+
"E_TOO_MANY_RECIPIENTS",
|
|
35
|
+
"E_TOO_MANY_ATTACHMENTS",
|
|
36
|
+
"E_SENDER_NOT_VERIFIED",
|
|
37
|
+
"E_RECIPIENT_NOT_ALLOWED",
|
|
38
|
+
"E_RECIPIENT_SUPPRESSED",
|
|
39
|
+
"E_SENDER_DOMAIN_NOT_AVAILABLE",
|
|
40
|
+
"E_CONTENT_TOO_LARGE",
|
|
41
|
+
"E_DELIVERY_FAILED",
|
|
42
|
+
"E_RATE_LIMIT_EXCEEDED",
|
|
43
|
+
"E_DAILY_LIMIT_EXCEEDED",
|
|
44
|
+
"E_INTERNAL_SERVER_ERROR",
|
|
45
|
+
"E_HEADER_NOT_ALLOWED",
|
|
46
|
+
"E_HEADER_USE_API_FIELD",
|
|
47
|
+
"E_HEADER_VALUE_INVALID",
|
|
48
|
+
"E_HEADER_VALUE_TOO_LONG",
|
|
49
|
+
"E_HEADER_NAME_INVALID",
|
|
50
|
+
"E_HEADERS_TOO_LARGE",
|
|
51
|
+
"E_HEADERS_TOO_MANY",
|
|
52
|
+
] as const;
|
|
53
|
+
|
|
54
|
+
/** A documented Cloudflare Email Sending error code. */
|
|
55
|
+
export type EmailErrorCode = (typeof emailErrorCodes)[number];
|
|
14
56
|
|
|
15
57
|
/** Error raised when a Cloudflare Send Email operation fails. */
|
|
16
58
|
export class EmailOperationError extends Data.TaggedError("EmailOperationError")<{
|
|
17
59
|
readonly binding: string;
|
|
18
60
|
readonly operation: string;
|
|
19
61
|
readonly cause: unknown;
|
|
62
|
+
/** Cloudflare error code taken from the thrown error, when present. */
|
|
63
|
+
readonly code?: EmailErrorCode | (string & {});
|
|
64
|
+
}> {}
|
|
65
|
+
|
|
66
|
+
/** A single message field that violates a documented Email Sending limit. */
|
|
67
|
+
export interface EmailViolation {
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly message: string;
|
|
70
|
+
readonly limit?: number;
|
|
71
|
+
readonly actual?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Error raised when a message violates documented Cloudflare Email Sending limits. */
|
|
75
|
+
export class EmailValidationError extends Data.TaggedError("EmailValidationError")<{
|
|
76
|
+
readonly binding: string;
|
|
77
|
+
readonly operation: string;
|
|
78
|
+
readonly violations: ReadonlyArray<EmailViolation>;
|
|
20
79
|
}> {}
|
|
21
80
|
|
|
22
81
|
/** Typed Cloudflare Send Email binding definition. */
|
|
@@ -32,6 +91,13 @@ export type EmailMessageBuilder = Parameters<CloudflareSendEmail["send"]>[0];
|
|
|
32
91
|
export type EmailSendInput = EmailMessage | EmailMessageBuilder;
|
|
33
92
|
export type EmailSendResult = CloudflareEmailSendResult;
|
|
34
93
|
export type EmailBinding = CloudflareSendEmail;
|
|
94
|
+
export type EmailRecipients = string | EmailAddress | ReadonlyArray<string | EmailAddress>;
|
|
95
|
+
export type EmailSendError = EmailOperationError | EmailValidationError;
|
|
96
|
+
|
|
97
|
+
export interface EmailSendOptions {
|
|
98
|
+
/** Validates builder messages against documented limits. Defaults to `true`. */
|
|
99
|
+
readonly validate?: boolean;
|
|
100
|
+
}
|
|
35
101
|
|
|
36
102
|
interface EmailRuntimeBinding {
|
|
37
103
|
readonly send: (message: EmailSendInput) => Promise<EmailSendResult>;
|
|
@@ -39,8 +105,8 @@ interface EmailRuntimeBinding {
|
|
|
39
105
|
|
|
40
106
|
export interface EmailClient {
|
|
41
107
|
readonly send: {
|
|
42
|
-
(message: EmailMessage): Effect.Effect<EmailSendResult,
|
|
43
|
-
(builder: EmailMessageBuilder): Effect.Effect<EmailSendResult,
|
|
108
|
+
(message: EmailMessage): Effect.Effect<EmailSendResult, EmailSendError>;
|
|
109
|
+
(builder: EmailMessageBuilder): Effect.Effect<EmailSendResult, EmailSendError>;
|
|
44
110
|
};
|
|
45
111
|
readonly unsafeRaw: Effect.Effect<EmailBinding>;
|
|
46
112
|
readonly definition: EmailDefinition;
|
|
@@ -57,6 +123,7 @@ export interface EmailService<Id extends string> {
|
|
|
57
123
|
|
|
58
124
|
export type LayerOptions = {
|
|
59
125
|
readonly binding: string;
|
|
126
|
+
readonly send?: EmailSendOptions;
|
|
60
127
|
};
|
|
61
128
|
|
|
62
129
|
export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
|
|
@@ -74,8 +141,24 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
|
|
|
74
141
|
>;
|
|
75
142
|
}
|
|
76
143
|
|
|
144
|
+
const emailErrorCode = (cause: unknown): EmailOperationError["code"] => {
|
|
145
|
+
if (typeof cause !== "object" || cause === null) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const code = Reflect.get(cause, "code");
|
|
150
|
+
|
|
151
|
+
return typeof code === "string" ? code : undefined;
|
|
152
|
+
};
|
|
153
|
+
|
|
77
154
|
const emailError = (binding: string, operation: string, cause: unknown) =>
|
|
78
|
-
new EmailOperationError({ binding, operation, cause });
|
|
155
|
+
new EmailOperationError({ binding, operation, cause, code: emailErrorCode(cause) });
|
|
156
|
+
|
|
157
|
+
const emailValidationError = (
|
|
158
|
+
binding: string,
|
|
159
|
+
operation: string,
|
|
160
|
+
violations: ReadonlyArray<EmailViolation>,
|
|
161
|
+
) => new EmailValidationError({ binding, operation, violations });
|
|
79
162
|
|
|
80
163
|
const tryEmailPromise = <A>(
|
|
81
164
|
binding: string,
|
|
@@ -87,6 +170,260 @@ const tryEmailPromise = <A>(
|
|
|
87
170
|
catch: (cause) => emailError(binding, operation, cause),
|
|
88
171
|
});
|
|
89
172
|
|
|
173
|
+
const builderFields = [
|
|
174
|
+
"subject",
|
|
175
|
+
"html",
|
|
176
|
+
"text",
|
|
177
|
+
"cc",
|
|
178
|
+
"bcc",
|
|
179
|
+
"replyTo",
|
|
180
|
+
"attachments",
|
|
181
|
+
"headers",
|
|
182
|
+
] as const;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Distinguishes structured Email Sending messages from raw MIME `EmailMessage`
|
|
186
|
+
* values, which only carry envelope `from` and `to` addresses.
|
|
187
|
+
*/
|
|
188
|
+
export const isEmailMessageBuilder = (message: EmailSendInput): message is EmailMessageBuilder => {
|
|
189
|
+
if (typeof message !== "object" || message === null) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return builderFields.some((field) => Reflect.get(message, field) !== undefined);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const byteLength = (value: string) => textEncoder.encode(value).byteLength;
|
|
197
|
+
|
|
198
|
+
const limitViolation = (
|
|
199
|
+
path: string,
|
|
200
|
+
label: string,
|
|
201
|
+
actual: number,
|
|
202
|
+
limit: number,
|
|
203
|
+
): EmailViolation => ({
|
|
204
|
+
path,
|
|
205
|
+
message: `${label} exceeds Cloudflare Email Sending limit`,
|
|
206
|
+
actual,
|
|
207
|
+
limit,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const addressList = (value: EmailRecipients | undefined): ReadonlyArray<string | EmailAddress> => {
|
|
211
|
+
if (value === undefined) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return Array.isArray(value) ? value : [value as string | EmailAddress];
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const addressViolations = (path: string, value: unknown): ReadonlyArray<EmailViolation> => {
|
|
219
|
+
if (typeof value === "string") {
|
|
220
|
+
return value.includes("@") ? [] : [{ path, message: "Email address must contain an @ symbol" }];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (typeof value !== "object" || value === null) {
|
|
224
|
+
return [{ path, message: "Email address must be a string or { name, email } object" }];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const email = Reflect.get(value, "email");
|
|
228
|
+
|
|
229
|
+
if (typeof email !== "string" || !email.includes("@")) {
|
|
230
|
+
return [{ path: `${path}.email`, message: "Email address must contain an @ symbol" }];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return [];
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const recipientViolations = (
|
|
237
|
+
field: "to" | "cc" | "bcc",
|
|
238
|
+
value: EmailRecipients | undefined,
|
|
239
|
+
): ReadonlyArray<EmailViolation> =>
|
|
240
|
+
addressList(value).flatMap((address, index) => addressViolations(`${field}[${index}]`, address));
|
|
241
|
+
|
|
242
|
+
const attachmentViolations = (
|
|
243
|
+
attachments: ReadonlyArray<EmailAttachment>,
|
|
244
|
+
): ReadonlyArray<EmailViolation> => {
|
|
245
|
+
const violations: Array<EmailViolation> = [];
|
|
246
|
+
|
|
247
|
+
if (attachments.length > sendLimits.maxAttachments) {
|
|
248
|
+
violations.push(
|
|
249
|
+
limitViolation("attachments", "attachments", attachments.length, sendLimits.maxAttachments),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (let index = 0; index < attachments.length; index++) {
|
|
254
|
+
const attachment = attachments[index];
|
|
255
|
+
const path = `attachments[${index}]`;
|
|
256
|
+
|
|
257
|
+
if (typeof attachment !== "object" || attachment === null) {
|
|
258
|
+
violations.push({ path, message: "Attachment must be an object" });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (typeof attachment.filename !== "string" || attachment.filename === "") {
|
|
263
|
+
violations.push({ path: `${path}.filename`, message: "Attachment filename is required" });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (typeof attachment.type !== "string" || attachment.type === "") {
|
|
267
|
+
violations.push({ path: `${path}.type`, message: "Attachment MIME type is required" });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (attachment.content === undefined || attachment.content === null) {
|
|
271
|
+
violations.push({ path: `${path}.content`, message: "Attachment content is required" });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (attachment.disposition === "inline" && typeof attachment.contentId !== "string") {
|
|
275
|
+
violations.push({
|
|
276
|
+
path: `${path}.contentId`,
|
|
277
|
+
message: "Inline attachments require a contentId",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return violations;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const headerViolations = (headers: Record<string, string>): ReadonlyArray<EmailViolation> => {
|
|
286
|
+
const violations: Array<EmailViolation> = [];
|
|
287
|
+
const entries = Object.entries(headers);
|
|
288
|
+
let totalBytes = 0;
|
|
289
|
+
let allowlistHeaders = 0;
|
|
290
|
+
|
|
291
|
+
for (const [name, value] of entries) {
|
|
292
|
+
const path = `headers["${name}"]`;
|
|
293
|
+
const nameBytes = byteLength(name);
|
|
294
|
+
|
|
295
|
+
if (nameBytes > sendLimits.maxHeaderNameBytes) {
|
|
296
|
+
violations.push(
|
|
297
|
+
limitViolation(path, "header name bytes", nameBytes, sendLimits.maxHeaderNameBytes),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (typeof value !== "string") {
|
|
302
|
+
violations.push({ path, message: "Header value must be a string" });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const valueBytes = byteLength(value);
|
|
307
|
+
|
|
308
|
+
if (valueBytes > sendLimits.maxHeaderValueBytes) {
|
|
309
|
+
violations.push(
|
|
310
|
+
limitViolation(path, "header value bytes", valueBytes, sendLimits.maxHeaderValueBytes),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
totalBytes += nameBytes + valueBytes;
|
|
315
|
+
|
|
316
|
+
if (!name.toLowerCase().startsWith("x-")) {
|
|
317
|
+
allowlistHeaders += 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (totalBytes > sendLimits.maxHeadersBytes) {
|
|
322
|
+
violations.push(
|
|
323
|
+
limitViolation("headers", "header bytes", totalBytes, sendLimits.maxHeadersBytes),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (allowlistHeaders > sendLimits.maxAllowlistHeaders) {
|
|
328
|
+
violations.push(
|
|
329
|
+
limitViolation(
|
|
330
|
+
"headers",
|
|
331
|
+
"allowlisted custom headers",
|
|
332
|
+
allowlistHeaders,
|
|
333
|
+
sendLimits.maxAllowlistHeaders,
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return violations;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Validates a structured message against documented Cloudflare Email Sending
|
|
343
|
+
* limits and returns every violation found. Total message size is not checked
|
|
344
|
+
* because the encoded MIME size is only known once Cloudflare composes it.
|
|
345
|
+
*/
|
|
346
|
+
export const validateMessage = (message: EmailMessageBuilder): ReadonlyArray<EmailViolation> => {
|
|
347
|
+
const violations: Array<EmailViolation> = [];
|
|
348
|
+
const fields = message as {
|
|
349
|
+
readonly to?: EmailRecipients;
|
|
350
|
+
readonly cc?: EmailRecipients;
|
|
351
|
+
readonly bcc?: EmailRecipients;
|
|
352
|
+
readonly from?: unknown;
|
|
353
|
+
readonly replyTo?: unknown;
|
|
354
|
+
readonly subject?: unknown;
|
|
355
|
+
readonly text?: unknown;
|
|
356
|
+
readonly html?: unknown;
|
|
357
|
+
readonly attachments?: ReadonlyArray<EmailAttachment>;
|
|
358
|
+
readonly headers?: Record<string, string>;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (fields.from === undefined) {
|
|
362
|
+
violations.push({ path: "from", message: "A from address is required" });
|
|
363
|
+
} else {
|
|
364
|
+
violations.push(...addressViolations("from", fields.from));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (fields.replyTo !== undefined) {
|
|
368
|
+
violations.push(...addressViolations("replyTo", fields.replyTo));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (typeof fields.subject !== "string" || fields.subject === "") {
|
|
372
|
+
violations.push({ path: "subject", message: "A subject is required" });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (typeof fields.text !== "string" && typeof fields.html !== "string") {
|
|
376
|
+
violations.push({ path: "$", message: "A message requires either text or html content" });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const recipients =
|
|
380
|
+
addressList(fields.to).length + addressList(fields.cc).length + addressList(fields.bcc).length;
|
|
381
|
+
|
|
382
|
+
if (recipients === 0) {
|
|
383
|
+
violations.push({
|
|
384
|
+
path: "$",
|
|
385
|
+
message: "A message requires at least one to, cc, or bcc address",
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (recipients > sendLimits.maxRecipients) {
|
|
390
|
+
violations.push(
|
|
391
|
+
limitViolation(
|
|
392
|
+
"$",
|
|
393
|
+
"combined to, cc, and bcc addresses",
|
|
394
|
+
recipients,
|
|
395
|
+
sendLimits.maxRecipients,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
violations.push(...recipientViolations("to", fields.to));
|
|
401
|
+
violations.push(...recipientViolations("cc", fields.cc));
|
|
402
|
+
violations.push(...recipientViolations("bcc", fields.bcc));
|
|
403
|
+
|
|
404
|
+
if (fields.attachments !== undefined) {
|
|
405
|
+
violations.push(...attachmentViolations(fields.attachments));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (fields.headers !== undefined) {
|
|
409
|
+
violations.push(...headerViolations(fields.headers));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return violations;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const validateSendInput = (
|
|
416
|
+
binding: string,
|
|
417
|
+
operation: string,
|
|
418
|
+
message: EmailMessageBuilder,
|
|
419
|
+
): Effect.Effect<EmailMessageBuilder, EmailValidationError> => {
|
|
420
|
+
const violations = validateMessage(message);
|
|
421
|
+
|
|
422
|
+
return violations.length > 0
|
|
423
|
+
? Effect.fail(emailValidationError(binding, operation, violations))
|
|
424
|
+
: Effect.succeed(message);
|
|
425
|
+
};
|
|
426
|
+
|
|
90
427
|
export const isEmailBinding = (value: unknown): value is EmailBinding => {
|
|
91
428
|
if (typeof value !== "object" || value === null) {
|
|
92
429
|
return false;
|
|
@@ -98,13 +435,20 @@ export const isEmailBinding = (value: unknown): value is EmailBinding => {
|
|
|
98
435
|
};
|
|
99
436
|
|
|
100
437
|
export const makeClient =
|
|
101
|
-
(definition: EmailDefinition) =>
|
|
438
|
+
(definition: EmailDefinition, options?: EmailSendOptions) =>
|
|
102
439
|
(email: EmailBinding): EmailClient => {
|
|
103
440
|
const runtime = email as EmailRuntimeBinding;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
441
|
+
const validate = options?.validate ?? true;
|
|
442
|
+
const send = ((message: EmailSendInput) => {
|
|
443
|
+
const dispatch = () =>
|
|
444
|
+
tryEmailPromise(definition.binding, "send", () => runtime.send(message));
|
|
445
|
+
|
|
446
|
+
if (!validate || !isEmailMessageBuilder(message)) {
|
|
447
|
+
return dispatch();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return validateSendInput(definition.binding, "send", message).pipe(Effect.flatMap(dispatch));
|
|
451
|
+
}) as EmailClient["send"];
|
|
108
452
|
|
|
109
453
|
return {
|
|
110
454
|
definition,
|
|
@@ -113,8 +457,8 @@ export const makeClient =
|
|
|
113
457
|
};
|
|
114
458
|
};
|
|
115
459
|
|
|
116
|
-
export const layer = <Self>(tag: Context.Service<Self, EmailClient>, definition:
|
|
117
|
-
Binding.layer(tag, definition.binding, isEmailBinding, makeClient(definition), {
|
|
460
|
+
export const layer = <Self>(tag: Context.Service<Self, EmailClient>, definition: LayerOptions) =>
|
|
461
|
+
Binding.layer(tag, definition.binding, isEmailBinding, makeClient(definition, definition.send), {
|
|
118
462
|
expected: expectedSendEmailBinding,
|
|
119
463
|
});
|
|
120
464
|
|
package/src/HyperdrivePg.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PgClient } from "@effect/sql-pg";
|
|
2
2
|
import { Effect, Layer, Redacted } from "effect";
|
|
3
3
|
|
|
4
|
-
import * as Hyperdrive from "./Hyperdrive";
|
|
4
|
+
import type * as Hyperdrive from "./Hyperdrive";
|
|
5
5
|
|
|
6
6
|
export type PgLayerOptions = Omit<
|
|
7
7
|
PgClient.PgClientConfig & { readonly acquireForStream?: boolean },
|
package/src/Kv.ts
CHANGED
|
@@ -124,7 +124,7 @@ export interface TagClass<
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
const maybeString = (value: string | null | undefined): Option.Option<string> =>
|
|
127
|
-
value
|
|
127
|
+
value === null || value === undefined ? Option.none() : Option.some(value);
|
|
128
128
|
|
|
129
129
|
const maybeNumber = (value: number | undefined): Option.Option<number> =>
|
|
130
130
|
value === undefined ? Option.none() : Option.some(value);
|
|
@@ -171,6 +171,7 @@ export const makeClient = <Key, Value, EncodedValue>(
|
|
|
171
171
|
put: Effect.fnUntraced(function* (key: Key, value: Value, options?: KvPutOptions) {
|
|
172
172
|
const keyEncoded = yield* encodeKey(key);
|
|
173
173
|
const valueEncoded = yield* encodeValue(value);
|
|
174
|
+
|
|
174
175
|
yield* tryKvPromise(definition.binding, "put", () =>
|
|
175
176
|
kv.put(keyEncoded, valueEncoded, options),
|
|
176
177
|
);
|
|
@@ -242,6 +243,7 @@ export const makeClient = <Key, Value, EncodedValue>(
|
|
|
242
243
|
}),
|
|
243
244
|
remove: Effect.fnUntraced(function* (key: Key) {
|
|
244
245
|
const keyEncoded = yield* encodeKey(key);
|
|
246
|
+
|
|
245
247
|
yield* tryKvPromise(definition.binding, "delete", () => kv.delete(keyEncoded));
|
|
246
248
|
}),
|
|
247
249
|
unsafeRaw: Effect.succeed(kv),
|
package/src/QueueBinding.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
QueueSendOptions as CloudflareQueueSendOptions,
|
|
8
8
|
QueueSendResponse as CloudflareQueueSendResponse,
|
|
9
9
|
} from "@cloudflare/workers-types";
|
|
10
|
-
import { Context, Data, Effect, Schema as S } from "effect";
|
|
10
|
+
import { type Context, Data, Effect, Schema as S } from "effect";
|
|
11
11
|
|
|
12
12
|
import * as Binding from "./Binding";
|
|
13
13
|
import type * as RpcDefinition from "./RpcDefinition";
|
|
@@ -110,6 +110,7 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
|
|
|
110
110
|
yield* tryQueuePromise(definition.binding, "sendBatch", () =>
|
|
111
111
|
sendBatch.call(queue, encodedMessages, options),
|
|
112
112
|
);
|
|
113
|
+
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
package/src/QueueDefinition.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Schema as S, type Layer, type Scope } from "effect";
|
|
2
2
|
|
|
3
|
-
import * as Binding from "./Binding";
|
|
3
|
+
import type * as Binding from "./Binding";
|
|
4
4
|
import type { WorkerEnvironment } from "./Environment";
|
|
5
5
|
import * as QueueEntrypoint from "./Queue";
|
|
6
6
|
import * as QueueBinding from "./QueueBinding";
|
|
@@ -121,6 +121,7 @@ export const Tag =
|
|
|
121
121
|
options?: QueueBinding.QueueSendOptions,
|
|
122
122
|
) {
|
|
123
123
|
const queue = yield* tag;
|
|
124
|
+
|
|
124
125
|
yield* queue.send(message, options);
|
|
125
126
|
});
|
|
126
127
|
|
|
@@ -129,16 +130,19 @@ export const Tag =
|
|
|
129
130
|
options?: QueueBinding.QueueSendBatchOptions,
|
|
130
131
|
) {
|
|
131
132
|
const queue = yield* tag;
|
|
133
|
+
|
|
132
134
|
yield* queue.sendBatch(messages, options);
|
|
133
135
|
});
|
|
134
136
|
|
|
135
137
|
const metrics = Effect.fnUntraced(function* () {
|
|
136
138
|
const queue = yield* tag;
|
|
139
|
+
|
|
137
140
|
return yield* queue.metrics();
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
const unsafeRaw = Effect.fnUntraced(function* () {
|
|
141
144
|
const queue = yield* tag;
|
|
145
|
+
|
|
142
146
|
return yield* queue.unsafeRaw;
|
|
143
147
|
});
|
|
144
148
|
|
|
@@ -165,6 +169,7 @@ const wrapHandler = <ROut, const Self extends Definition.Any>(
|
|
|
165
169
|
return (batch) =>
|
|
166
170
|
Effect.gen(function* () {
|
|
167
171
|
const decoded = yield* QueueEntrypoint.decodeBatch(batch.raw, decodeBody);
|
|
172
|
+
|
|
168
173
|
yield* handler(decoded);
|
|
169
174
|
});
|
|
170
175
|
};
|
package/src/R2.ts
CHANGED
|
@@ -280,6 +280,7 @@ export const makeClient = (
|
|
|
280
280
|
),
|
|
281
281
|
delete: (keys) => {
|
|
282
282
|
const nativeKeys = typeof keys === "string" ? keys : [...keys];
|
|
283
|
+
|
|
283
284
|
return tryR2Promise(definition.binding, "delete", () => bucket.delete(nativeKeys));
|
|
284
285
|
},
|
|
285
286
|
list: (options) => tryR2Promise(definition.binding, "list", () => bucket.list(options)),
|
package/src/RpcDefinition.ts
CHANGED
|
@@ -218,6 +218,7 @@ export const decodeArgs = <
|
|
|
218
218
|
|
|
219
219
|
for (let index = 0; index < methodDefinition.args.length; index++) {
|
|
220
220
|
const schema = methodDefinition.args[index];
|
|
221
|
+
|
|
221
222
|
decoded.push(
|
|
222
223
|
yield* (S.decodeUnknownEffect(schema)(args[index]) as Effect.Effect<unknown, unknown>).pipe(
|
|
223
224
|
Effect.mapError(
|
|
@@ -265,6 +266,7 @@ export const encodeArgs = <
|
|
|
265
266
|
|
|
266
267
|
for (let index = 0; index < methodDefinition.args.length; index++) {
|
|
267
268
|
const schema = methodDefinition.args[index];
|
|
269
|
+
|
|
268
270
|
encoded.push(
|
|
269
271
|
yield* (S.encodeEffect(schema)(args[index]) as Effect.Effect<unknown, unknown>).pipe(
|
|
270
272
|
Effect.mapError(
|
package/src/ServiceBinding.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Data, Effect, type Context, type Scope } from "effect";
|
|
2
2
|
|
|
3
3
|
import * as Binding from "./Binding";
|
|
4
4
|
import * as CloudflareRpc from "./Rpc";
|
|
@@ -333,6 +333,7 @@ export const makeClient = <
|
|
|
333
333
|
const methodName = String(method);
|
|
334
334
|
const result = yield* rpc(method, ...args);
|
|
335
335
|
const value = yield* CloudflareRpc.scoped(result);
|
|
336
|
+
|
|
336
337
|
return yield* decodeSuccess<Method>(methodName, value);
|
|
337
338
|
});
|
|
338
339
|
|
|
@@ -413,6 +414,7 @@ export const Service =
|
|
|
413
414
|
const fetch = (input: RequestInfo | URL, init?: RequestInit) =>
|
|
414
415
|
Effect.gen(function* () {
|
|
415
416
|
const service = yield* tag;
|
|
417
|
+
|
|
416
418
|
return yield* service.fetch(input, init);
|
|
417
419
|
});
|
|
418
420
|
|
|
@@ -422,6 +424,7 @@ export const Service =
|
|
|
422
424
|
) =>
|
|
423
425
|
Effect.gen(function* () {
|
|
424
426
|
const service = yield* tag;
|
|
427
|
+
|
|
425
428
|
return yield* service.rpc(method, ...args);
|
|
426
429
|
});
|
|
427
430
|
|
|
@@ -431,6 +434,7 @@ export const Service =
|
|
|
431
434
|
) =>
|
|
432
435
|
Effect.gen(function* () {
|
|
433
436
|
const service = yield* tag;
|
|
437
|
+
|
|
434
438
|
return yield* service.call(method, ...args);
|
|
435
439
|
});
|
|
436
440
|
|
|
@@ -440,6 +444,7 @@ export const Service =
|
|
|
440
444
|
) =>
|
|
441
445
|
Effect.gen(function* () {
|
|
442
446
|
const service = yield* tag;
|
|
447
|
+
|
|
443
448
|
return yield* service.scopedCall(method, ...args);
|
|
444
449
|
});
|
|
445
450
|
|
package/src/Worker.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { WorkerEntrypoint as CloudflareWorkerEntrypoint } from "cloudflare:workers";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
type Cause,
|
|
4
|
+
ConfigProvider,
|
|
5
|
+
Context,
|
|
6
|
+
Effect,
|
|
7
|
+
Layer,
|
|
8
|
+
ManagedRuntime,
|
|
9
|
+
type Schema as S,
|
|
10
|
+
type Scope,
|
|
11
|
+
} from "effect";
|
|
4
12
|
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
5
13
|
|
|
6
14
|
import type * as Binding from "./Binding";
|