effect-cf 0.26.0 → 0.28.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 +16 -0
- package/README.md +8 -6
- package/dist/{Artifacts-DcoXcRP5.d.mts → Artifacts-DnHQA-Ax.d.mts} +42 -25
- package/dist/Artifacts-rq3hUKQE.mjs +257 -0
- package/dist/{Binding-COCtoJXa.d.mts → Binding-xJJJPmiD.d.mts} +6 -5
- package/dist/ComputerArtifacts.d.mts +55 -0
- package/dist/ComputerArtifacts.mjs +95 -0
- package/dist/ComputerWorkspace.d.mts +279 -0
- package/dist/ComputerWorkspace.mjs +577 -0
- package/dist/ComputerWorkspaceHost.d.mts +1 -1
- package/dist/ComputerWorkspaceHost.mjs +18 -16
- package/dist/{WorkflowBinding-D5iWzCUa.d.mts → DurableObjectState-B3uFY1eM.d.mts} +90 -141
- package/dist/{DurableObjectState-CEh0DVw7.mjs → DurableObjectState-BjkK2kwH.mjs} +4 -49
- package/dist/ErrorMessage-BM0K-Nik.mjs +46 -0
- package/dist/{Hyperdrive-6p59yyVk.d.mts → Hyperdrive-z-9fFz15.d.mts} +2 -2
- package/dist/HyperdrivePg.d.mts +2 -2
- package/dist/Vitest.d.mts +6 -4
- package/dist/Vitest.mjs +2 -1
- package/dist/WorkflowBinding-wZ-yGHow.d.mts +74 -0
- package/dist/index.d.mts +92 -398
- package/dist/index.mjs +323 -1105
- package/package.json +12 -1
- package/src/AiGateway.ts +62 -7
- package/src/AnalyticsEngine.ts +17 -13
- package/src/Artifacts.ts +111 -47
- package/src/Binding.ts +34 -23
- package/src/BrowserRendering.ts +72 -33
- package/src/CloudflareOtlp.ts +8 -5
- package/src/ComputerArtifacts.ts +17 -9
- package/src/ComputerWorkspace.ts +202 -98
- package/src/ComputerWorkspaceHost.ts +55 -27
- package/src/ContainerNamespace.ts +8 -9
- package/src/D1.ts +8 -14
- package/src/DurableObject.ts +27 -11
- package/src/DurableObjectAlarm.ts +17 -5
- package/src/DurableObjectDefinition.ts +89 -72
- package/src/DurableObjectNamespace.ts +26 -12
- package/src/DurableObjectRpcWebSocket.ts +9 -10
- package/src/DurableObjectState.ts +1 -0
- package/src/DurableObjectStorage.ts +5 -2
- package/src/DurableObjectWebSocket.ts +4 -1
- package/src/Email.ts +26 -25
- package/src/Hyperdrive.ts +4 -10
- package/src/Images.ts +12 -14
- package/src/Kv.ts +15 -17
- package/src/Queue.ts +2 -1
- package/src/QueueBinding.ts +7 -12
- package/src/QueueDefinition.ts +7 -2
- package/src/R2.ts +33 -24
- package/src/Rpc.ts +15 -21
- package/src/RpcDefinition.ts +33 -38
- package/src/ServiceBinding.ts +26 -8
- package/src/Vectorize.ts +45 -18
- package/src/Vitest.ts +9 -3
- package/src/WebTransport.ts +11 -5
- package/src/Worker.ts +91 -21
- package/src/WorkerDefinition.ts +143 -63
- package/src/WorkersAi.ts +50 -28
- package/src/Workflow.ts +66 -13
- package/src/WorkflowBinding.ts +18 -25
- package/src/WorkflowDefinition.ts +4 -2
- package/src/index.ts +0 -2
- package/src/internal/Entrypoint.ts +29 -13
- package/src/internal/ErrorMessage.ts +7 -6
- package/src/internal/RpcInvocation.ts +34 -11
- package/src/internal/Runtime.ts +33 -22
- package/src/internal/WorkerContext.ts +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Data, Effect, type Context, type Layer, type Scope } from "effect";
|
|
1
|
+
import { Data, Effect, Predicate, type Context, type Layer, type Scope } from "effect";
|
|
2
2
|
|
|
3
3
|
import * as Binding from "./Binding";
|
|
4
4
|
import type { WorkerEnvironment } from "./Environment";
|
|
@@ -17,13 +17,18 @@ interface DurableObjectFetcher {
|
|
|
17
17
|
|
|
18
18
|
type RpcClient<Api> = {
|
|
19
19
|
readonly [Key in keyof Api as Key extends string
|
|
20
|
-
? Api[Key] extends (...args: Array<any>) =>
|
|
20
|
+
? Api[Key] extends (...args: Array<any>) => any
|
|
21
21
|
? Key
|
|
22
22
|
: never
|
|
23
23
|
: never]: Api[Key];
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
type ReservedMethodName = DurableObjectDefinition.ReservedMethodName | "fetch";
|
|
27
|
+
type DynamicMethod = (...args: Array<any>) => any;
|
|
28
|
+
interface DynamicMethods {
|
|
29
|
+
[method: string]: DynamicMethod;
|
|
30
|
+
}
|
|
31
|
+
type NamespaceCandidate = Parameters<typeof Predicate.isUnknown>[0];
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Cloudflare Durable Object stub, optionally enriched with RPC methods.
|
|
@@ -373,14 +378,12 @@ export type DurableObjectNamespaceStaticClient<
|
|
|
373
378
|
readonly rawUnsafe: () => Effect.Effect<DurableObjectNamespaceClient<Api>, never, R>;
|
|
374
379
|
};
|
|
375
380
|
|
|
376
|
-
const hasFunction = (value:
|
|
377
|
-
|
|
381
|
+
const hasFunction = <Candidate>(value: Candidate, key: string): boolean =>
|
|
382
|
+
Predicate.hasProperty(value, key) && Predicate.isFunction(value[key]);
|
|
378
383
|
|
|
379
384
|
export const isDurableObjectNamespaceClient = <Api extends object>(
|
|
380
|
-
value:
|
|
385
|
+
value: NamespaceCandidate,
|
|
381
386
|
): value is DurableObjectNamespaceClient<Api> =>
|
|
382
|
-
typeof value === "object" &&
|
|
383
|
-
value !== null &&
|
|
384
387
|
hasFunction(value, "getByName") &&
|
|
385
388
|
hasFunction(value, "get") &&
|
|
386
389
|
hasFunction(value, "idFromName") &&
|
|
@@ -430,6 +433,7 @@ export const makeClient = <
|
|
|
430
433
|
Method extends StubMethodKey<Api>,
|
|
431
434
|
>(stub: StubClient, method: Method, ...args: StubMethodArgs<Api, Method>) {
|
|
432
435
|
const methodName = String(method);
|
|
436
|
+
// SAFETY: the runtime definition and method name select the same method schemas as Method.
|
|
433
437
|
const encodedArgs =
|
|
434
438
|
definition.definition === undefined
|
|
435
439
|
? args
|
|
@@ -448,6 +452,7 @@ export const makeClient = <
|
|
|
448
452
|
),
|
|
449
453
|
);
|
|
450
454
|
|
|
455
|
+
// SAFETY: encodedArgs preserves the selected method's positional argument contract.
|
|
451
456
|
return yield* RpcInvocation.invokeRpcMethod(
|
|
452
457
|
stub,
|
|
453
458
|
method,
|
|
@@ -465,9 +470,11 @@ export const makeClient = <
|
|
|
465
470
|
Method extends StubMethodKey<Api>,
|
|
466
471
|
>(methodName: string, value: Awaited<StubMethodCloudflareReturn<Api, Method>>) {
|
|
467
472
|
if (definition.definition === undefined) {
|
|
473
|
+
// SAFETY: without a schema codec, the native RPC result is already the declared success type.
|
|
468
474
|
return value as StubMethodSuccess<Api, Method>;
|
|
469
475
|
}
|
|
470
476
|
|
|
477
|
+
// SAFETY: methodName identifies Method in the same definition used to decode the result.
|
|
471
478
|
const decoded = yield* RpcDefinition.decodeSuccess(
|
|
472
479
|
definition.definition,
|
|
473
480
|
methodName as RpcDefinition.Definition.MethodNames<NonNullable<Definition>>,
|
|
@@ -483,6 +490,7 @@ export const makeClient = <
|
|
|
483
490
|
),
|
|
484
491
|
);
|
|
485
492
|
|
|
493
|
+
// SAFETY: the selected method's success schema decodes to its declared API success type.
|
|
486
494
|
return decoded as StubMethodSuccess<Api, Method>;
|
|
487
495
|
});
|
|
488
496
|
|
|
@@ -521,6 +529,7 @@ export const makeClient = <
|
|
|
521
529
|
getByName,
|
|
522
530
|
});
|
|
523
531
|
|
|
532
|
+
// SAFETY: the common namespace operations and generated direct methods implement the client contract.
|
|
524
533
|
return Object.assign(directMethods, {
|
|
525
534
|
newUniqueId,
|
|
526
535
|
idFromName,
|
|
@@ -581,47 +590,51 @@ export const makeDirectMethods = <
|
|
|
581
590
|
) => Effect.Effect<DurableObjectStubClient<Api>, never, R>;
|
|
582
591
|
},
|
|
583
592
|
): DirectMethods<R, Api, Definition> => {
|
|
584
|
-
const methods = {}
|
|
593
|
+
const methods: DynamicMethods = {};
|
|
585
594
|
|
|
586
595
|
if (rpcDefinition !== undefined) {
|
|
587
596
|
for (const methodName of Object.keys(rpcDefinition.methods)) {
|
|
597
|
+
// SAFETY: methodName comes from rpcDefinition.methods and helpers.call dispatches that same key.
|
|
588
598
|
methods[methodName] = (stub: DurableObjectStubClient<Api>, ...args: Array<unknown>) =>
|
|
589
599
|
(
|
|
590
600
|
helpers.call as (
|
|
591
601
|
stub: DurableObjectStubClient<Api>,
|
|
592
602
|
method: string,
|
|
593
603
|
...args: Array<unknown>
|
|
594
|
-
) =>
|
|
604
|
+
) => any
|
|
595
605
|
)(stub, methodName, ...args);
|
|
596
606
|
}
|
|
597
607
|
|
|
598
608
|
const makeClient = (
|
|
599
609
|
getStub: () => Effect.Effect<DurableObjectStubClient<Api>, never, R>,
|
|
600
610
|
): DurableObjectDirectClient<R, Api> => {
|
|
601
|
-
const client = {
|
|
611
|
+
const client: DynamicMethods = {
|
|
602
612
|
fetch: (input: RequestInfo | URL, init?: RequestInit) =>
|
|
603
613
|
Effect.gen(function* () {
|
|
604
614
|
const stub = yield* getStub();
|
|
605
615
|
|
|
606
616
|
return yield* helpers.fetch(stub, input, init);
|
|
607
617
|
}),
|
|
608
|
-
}
|
|
618
|
+
};
|
|
609
619
|
|
|
610
620
|
for (const methodName of Object.keys(rpcDefinition.methods)) {
|
|
621
|
+
// SAFETY: methodName comes from rpcDefinition.methods and helpers.call dispatches that same key.
|
|
611
622
|
client[methodName] = (...args: Array<unknown>) =>
|
|
612
623
|
Effect.gen(function* () {
|
|
613
624
|
const stub = yield* getStub();
|
|
614
625
|
|
|
626
|
+
// SAFETY: methodName and arguments come from the same generated RPC method entry.
|
|
615
627
|
return yield* (
|
|
616
628
|
helpers.call as (
|
|
617
629
|
stub: DurableObjectStubClient<Api>,
|
|
618
630
|
method: string,
|
|
619
631
|
...args: Array<unknown>
|
|
620
|
-
) => Effect.Effect<
|
|
632
|
+
) => Effect.Effect<any, DurableObjectRpcError, R>
|
|
621
633
|
)(stub, methodName, ...args);
|
|
622
634
|
});
|
|
623
635
|
}
|
|
624
636
|
|
|
637
|
+
// SAFETY: client contains fetch plus every RPC method enumerated by rpcDefinition.
|
|
625
638
|
return client as DurableObjectDirectClient<R, Api>;
|
|
626
639
|
};
|
|
627
640
|
|
|
@@ -636,5 +649,6 @@ export const makeDirectMethods = <
|
|
|
636
649
|
) => makeClient(() => helpers.get(id, options));
|
|
637
650
|
}
|
|
638
651
|
|
|
652
|
+
// SAFETY: methods is populated from the exact rpcDefinition keys, including its by-name/id clients.
|
|
639
653
|
return methods as DirectMethods<R, Api, Definition>;
|
|
640
654
|
};
|
|
@@ -32,7 +32,7 @@ export interface DurableObjectRpcWebSocketService {
|
|
|
32
32
|
message: NativeWebSocketMessage,
|
|
33
33
|
) => Effect.Effect<void>;
|
|
34
34
|
readonly close: (socket: DurableWebSocket) => Effect.Effect<void>;
|
|
35
|
-
readonly error: (socket: DurableWebSocket,
|
|
35
|
+
readonly error: (socket: DurableWebSocket, cause: unknown) => Effect.Effect<void>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
interface RpcConnection {
|
|
@@ -192,6 +192,7 @@ export const layer = (
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
for (const current of decoded) {
|
|
195
|
+
// SAFETY: this parser was created by the configured RPC serialization service.
|
|
195
196
|
yield* run(connection.id, current as RpcMessage.FromClientEncoded);
|
|
196
197
|
}
|
|
197
198
|
}).pipe(
|
|
@@ -204,9 +205,9 @@ export const layer = (
|
|
|
204
205
|
}),
|
|
205
206
|
),
|
|
206
207
|
close: unregister,
|
|
207
|
-
error: (socket,
|
|
208
|
+
error: (socket, cause) =>
|
|
208
209
|
Effect.gen(function* () {
|
|
209
|
-
yield* Effect.logDebug("Durable Object RPC websocket error",
|
|
210
|
+
yield* Effect.logDebug("Durable Object RPC websocket error", cause);
|
|
210
211
|
yield* unregister(socket);
|
|
211
212
|
}),
|
|
212
213
|
});
|
|
@@ -219,16 +220,15 @@ export const layer = (
|
|
|
219
220
|
);
|
|
220
221
|
|
|
221
222
|
const normalizeMessage = (message: NativeWebSocketMessage) =>
|
|
222
|
-
|
|
223
|
+
Predicate.isString(message) ? message : new Uint8Array(message);
|
|
223
224
|
|
|
224
225
|
const readAttachment = (socket: WebSocket, key: string): number | undefined => {
|
|
225
226
|
const value = socket.deserializeAttachment();
|
|
226
227
|
|
|
227
228
|
if (
|
|
228
|
-
value
|
|
229
|
-
|
|
230
|
-
key
|
|
231
|
-
typeof value[key] === "number"
|
|
229
|
+
Predicate.isObjectOrArray(value) &&
|
|
230
|
+
Predicate.hasProperty(value, key) &&
|
|
231
|
+
Predicate.isNumber(value[key])
|
|
232
232
|
) {
|
|
233
233
|
return value[key];
|
|
234
234
|
}
|
|
@@ -238,8 +238,7 @@ const readAttachment = (socket: WebSocket, key: string): number | undefined => {
|
|
|
238
238
|
|
|
239
239
|
const writeAttachment = (socket: WebSocket, key: string, clientId: number) => {
|
|
240
240
|
const current = socket.deserializeAttachment();
|
|
241
|
-
const attachment =
|
|
242
|
-
current !== null && typeof current === "object" && !Array.isArray(current) ? current : {};
|
|
241
|
+
const attachment = Predicate.isObject(current) ? current : {};
|
|
243
242
|
|
|
244
243
|
socket.serializeAttachment({
|
|
245
244
|
...attachment,
|
|
@@ -88,6 +88,7 @@ export const fromDurableObjectState = (
|
|
|
88
88
|
const scheduleWaitUntil = makeWaitUntilScheduler("DurableObjectState.waitUntil", (promise) =>
|
|
89
89
|
state.waitUntil(promise),
|
|
90
90
|
);
|
|
91
|
+
// SAFETY: the runtime Effect.isEffect branch implements both overloads of the service method.
|
|
91
92
|
const waitUntil = (<A, E, R, R2 = never>(
|
|
92
93
|
input: Promise<unknown> | Effect.Effect<A, E, R>,
|
|
93
94
|
options?: WorkerContextWaitUntilOptions<E, R2>,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Data, Effect, Exit, Option, Schema as S } from "effect";
|
|
1
|
+
import { Data, Effect, Exit, Option, Predicate, Schema as S } from "effect";
|
|
2
2
|
|
|
3
3
|
import * as ErrorMessage from "./internal/ErrorMessage";
|
|
4
4
|
|
|
@@ -260,6 +260,7 @@ const fromSyncKvStorage = (kv: globalThis.SyncKvStorage): SyncKvStorage => ({
|
|
|
260
260
|
schemaBackedSyncKvStorage(kv, definition),
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
// SAFETY: every overload is dispatched by the native key form and wrapped with the same Effect error contract.
|
|
263
264
|
const fromDurableObjectTransaction = (
|
|
264
265
|
txn: globalThis.DurableObjectTransaction,
|
|
265
266
|
): DurableObjectTransaction =>
|
|
@@ -279,7 +280,7 @@ const fromDurableObjectTransaction = (
|
|
|
279
280
|
maybeOptions?: globalThis.DurableObjectPutOptions,
|
|
280
281
|
) =>
|
|
281
282
|
tryStoragePromise("transaction.put", () =>
|
|
282
|
-
|
|
283
|
+
Predicate.isString(keyOrEntries)
|
|
283
284
|
? txn.put(keyOrEntries, valueOrOptions as T, maybeOptions)
|
|
284
285
|
: txn.put(keyOrEntries, valueOrOptions as globalThis.DurableObjectPutOptions | undefined),
|
|
285
286
|
),
|
|
@@ -334,6 +335,7 @@ export const fromDurableObjectStorage = (
|
|
|
334
335
|
);
|
|
335
336
|
} catch (cause) {
|
|
336
337
|
if (Exit.isExit(cause) && Exit.isFailure(cause)) {
|
|
338
|
+
// SAFETY: Exit.isFailure proves this cause carries the closure's typed E failure.
|
|
337
339
|
return Effect.failCause(cause.cause) as Effect.Effect<
|
|
338
340
|
A,
|
|
339
341
|
E | StorageOperationError,
|
|
@@ -366,6 +368,7 @@ export const fromDurableObjectStorage = (
|
|
|
366
368
|
(value) => resume(Effect.succeed(value)),
|
|
367
369
|
(cause) => {
|
|
368
370
|
if (Exit.isExit(cause) && Exit.isFailure(cause)) {
|
|
371
|
+
// SAFETY: Exit.isFailure proves this cause carries the closure's typed E failure.
|
|
369
372
|
resume(
|
|
370
373
|
Effect.failCause(cause.cause) as Effect.Effect<A, E | StorageOperationError>,
|
|
371
374
|
);
|
|
@@ -61,6 +61,7 @@ export const fromWebSocket = <Attachment = unknown>(
|
|
|
61
61
|
const existing = wrappers.get(raw);
|
|
62
62
|
|
|
63
63
|
if (existing !== undefined) {
|
|
64
|
+
// SAFETY: Attachment is phantom; a wrapper's runtime behavior is independent of that type.
|
|
64
65
|
return existing as DurableWebSocket<Attachment>;
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -89,6 +90,7 @@ export const fromWebSocket = <Attachment = unknown>(
|
|
|
89
90
|
|
|
90
91
|
wrappers.set(raw, socket);
|
|
91
92
|
|
|
93
|
+
// SAFETY: Attachment is phantom; the newly created wrapper supports values through its generic serializer.
|
|
92
94
|
return socket as DurableWebSocket<Attachment>;
|
|
93
95
|
};
|
|
94
96
|
|
|
@@ -200,6 +202,7 @@ export const attachment = <const AttachmentSchema extends S.Codec<any, any, neve
|
|
|
200
202
|
Effect.mapError(
|
|
201
203
|
(cause) => new DurableWebSocketAttachmentError({ operation: "serialize", cause }),
|
|
202
204
|
),
|
|
205
|
+
// SAFETY: S.encodeEffect returns this codec's declared Encoded representation.
|
|
203
206
|
Effect.flatMap((encoded) => socket.serializeAttachment(encoded as Encoded)),
|
|
204
207
|
);
|
|
205
208
|
|
|
@@ -261,7 +264,7 @@ export interface DurableWebSocketHandlers<R = never, E = unknown> {
|
|
|
261
264
|
reason: string,
|
|
262
265
|
wasClean: boolean,
|
|
263
266
|
) => Effect.Effect<void, E, R>;
|
|
264
|
-
readonly error?: (socket: DurableWebSocket,
|
|
267
|
+
readonly error?: (socket: DurableWebSocket, cause: unknown) => Effect.Effect<void, E, R>;
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
/** Maps compact websocket lifecycle handler names to `DurableObject.make` options. */
|
package/src/Email.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
EmailSendResult as CloudflareEmailSendResult,
|
|
5
5
|
SendEmail as CloudflareSendEmail,
|
|
6
6
|
} from "@cloudflare/workers-types";
|
|
7
|
-
import { Context, Data, Effect, type Layer } from "effect";
|
|
7
|
+
import { Context, Data, Effect, type Layer, Predicate } from "effect";
|
|
8
8
|
|
|
9
9
|
import * as Binding from "./Binding";
|
|
10
10
|
import type { WorkerEnvironment } from "./Environment";
|
|
@@ -114,6 +114,7 @@ export type EmailSendResult = CloudflareEmailSendResult;
|
|
|
114
114
|
export type EmailBinding = CloudflareSendEmail;
|
|
115
115
|
export type EmailRecipients = string | EmailAddress | ReadonlyArray<string | EmailAddress>;
|
|
116
116
|
export type EmailSendError = EmailOperationError | EmailValidationError;
|
|
117
|
+
type EmailFieldCandidate = Parameters<typeof Predicate.isUnknown>[0];
|
|
117
118
|
|
|
118
119
|
export interface EmailSendOptions {
|
|
119
120
|
/** Validates builder messages against documented limits. Defaults to `true`. */
|
|
@@ -166,13 +167,13 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
|
|
|
166
167
|
* error and strips non-standard properties.
|
|
167
168
|
*/
|
|
168
169
|
const emailErrorCode = (cause: unknown): EmailOperationError["code"] => {
|
|
169
|
-
if (
|
|
170
|
+
if (!Predicate.isObject(cause)) {
|
|
170
171
|
return undefined;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
const code =
|
|
174
|
+
const code = Predicate.hasProperty(cause, "code") ? cause.code : undefined;
|
|
174
175
|
|
|
175
|
-
return
|
|
176
|
+
return Predicate.isString(code) ? code : undefined;
|
|
176
177
|
};
|
|
177
178
|
|
|
178
179
|
const emailError = (binding: string, operation: string, cause: unknown) =>
|
|
@@ -213,21 +214,25 @@ const addressList = (value: EmailRecipients | undefined): ReadonlyArray<string |
|
|
|
213
214
|
return [];
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
// SAFETY: the non-array branch is exactly the scalar member of EmailRecipients.
|
|
216
218
|
return Array.isArray(value) ? value : [value as string | EmailAddress];
|
|
217
219
|
};
|
|
218
220
|
|
|
219
|
-
const addressViolations = (
|
|
220
|
-
|
|
221
|
+
const addressViolations = (
|
|
222
|
+
path: string,
|
|
223
|
+
value: EmailFieldCandidate,
|
|
224
|
+
): ReadonlyArray<EmailViolation> => {
|
|
225
|
+
if (Predicate.isString(value)) {
|
|
221
226
|
return value.includes("@") ? [] : [{ path, message: "Email address must contain an @ symbol" }];
|
|
222
227
|
}
|
|
223
228
|
|
|
224
|
-
if (
|
|
229
|
+
if (!Predicate.isObject(value)) {
|
|
225
230
|
return [{ path, message: "Email address must be a string or { name, email } object" }];
|
|
226
231
|
}
|
|
227
232
|
|
|
228
|
-
const email =
|
|
233
|
+
const email = Predicate.hasProperty(value, "email") ? value.email : undefined;
|
|
229
234
|
|
|
230
|
-
if (
|
|
235
|
+
if (!Predicate.isString(email) || !email.includes("@")) {
|
|
231
236
|
return [{ path: `${path}.email`, message: "Email address must contain an @ symbol" }];
|
|
232
237
|
}
|
|
233
238
|
|
|
@@ -255,16 +260,16 @@ const attachmentViolations = (
|
|
|
255
260
|
const attachment = attachments[index];
|
|
256
261
|
const path = `attachments[${index}]`;
|
|
257
262
|
|
|
258
|
-
if (
|
|
263
|
+
if (!Predicate.isObject(attachment)) {
|
|
259
264
|
violations.push({ path, message: "Attachment must be an object" });
|
|
260
265
|
continue;
|
|
261
266
|
}
|
|
262
267
|
|
|
263
|
-
if (
|
|
268
|
+
if (!Predicate.isString(attachment.filename) || attachment.filename === "") {
|
|
264
269
|
violations.push({ path: `${path}.filename`, message: "Attachment filename is required" });
|
|
265
270
|
}
|
|
266
271
|
|
|
267
|
-
if (
|
|
272
|
+
if (!Predicate.isString(attachment.type) || attachment.type === "") {
|
|
268
273
|
violations.push({ path: `${path}.type`, message: "Attachment MIME type is required" });
|
|
269
274
|
}
|
|
270
275
|
|
|
@@ -272,7 +277,7 @@ const attachmentViolations = (
|
|
|
272
277
|
violations.push({ path: `${path}.content`, message: "Attachment content is required" });
|
|
273
278
|
}
|
|
274
279
|
|
|
275
|
-
if (attachment.disposition === "inline" &&
|
|
280
|
+
if (attachment.disposition === "inline" && !Predicate.isString(attachment.contentId)) {
|
|
276
281
|
violations.push({
|
|
277
282
|
path: `${path}.contentId`,
|
|
278
283
|
message: "Inline attachments require a contentId",
|
|
@@ -299,7 +304,7 @@ const headerViolations = (headers: Record<string, string>): ReadonlyArray<EmailV
|
|
|
299
304
|
);
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
if (
|
|
307
|
+
if (!Predicate.isString(value)) {
|
|
303
308
|
violations.push({ path, message: "Header value must be a string" });
|
|
304
309
|
continue;
|
|
305
310
|
}
|
|
@@ -346,6 +351,7 @@ const headerViolations = (headers: Record<string, string>): ReadonlyArray<EmailV
|
|
|
346
351
|
*/
|
|
347
352
|
export const validateMessage = (message: EmailMessageBuilder): ReadonlyArray<EmailViolation> => {
|
|
348
353
|
const violations: Array<EmailViolation> = [];
|
|
354
|
+
// SAFETY: this wider view exists solely to validate runtime callers that bypass TypeScript.
|
|
349
355
|
const fields = message as {
|
|
350
356
|
readonly to?: EmailRecipients;
|
|
351
357
|
readonly cc?: EmailRecipients;
|
|
@@ -369,11 +375,11 @@ export const validateMessage = (message: EmailMessageBuilder): ReadonlyArray<Ema
|
|
|
369
375
|
violations.push(...addressViolations("replyTo", fields.replyTo));
|
|
370
376
|
}
|
|
371
377
|
|
|
372
|
-
if (
|
|
378
|
+
if (!Predicate.isString(fields.subject) || fields.subject === "") {
|
|
373
379
|
violations.push({ path: "subject", message: "A subject is required" });
|
|
374
380
|
}
|
|
375
381
|
|
|
376
|
-
if (
|
|
382
|
+
if (!Predicate.isString(fields.text) && !Predicate.isString(fields.html)) {
|
|
377
383
|
violations.push({ path: "$", message: "A message requires either text or html content" });
|
|
378
384
|
}
|
|
379
385
|
|
|
@@ -425,19 +431,13 @@ const validateSendInput = (
|
|
|
425
431
|
: Effect.succeed(message);
|
|
426
432
|
};
|
|
427
433
|
|
|
428
|
-
export const isEmailBinding = (value:
|
|
429
|
-
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const resource = value as Record<string, unknown>;
|
|
434
|
-
|
|
435
|
-
return typeof resource.send === "function";
|
|
436
|
-
};
|
|
434
|
+
export const isEmailBinding = <Candidate>(value: Candidate): value is Candidate & EmailBinding =>
|
|
435
|
+
Predicate.hasProperty(value, "send") && Predicate.isFunction(value.send);
|
|
437
436
|
|
|
438
437
|
export const makeClient =
|
|
439
438
|
(definition: EmailDefinition, options?: EmailSendOptions) =>
|
|
440
439
|
(email: EmailBinding): EmailClient => {
|
|
440
|
+
// SAFETY: EmailBinding.send is the native promise API represented by EmailRuntimeBinding.
|
|
441
441
|
const runtime = email as EmailRuntimeBinding;
|
|
442
442
|
const validate = options?.validate ?? true;
|
|
443
443
|
const send = Effect.fn("Email.send")(function* (message: EmailMessageBuilder) {
|
|
@@ -469,6 +469,7 @@ export const Tag =
|
|
|
469
469
|
|
|
470
470
|
const makeLayer = (definition: LayerOptions) => layer(tag, definition);
|
|
471
471
|
|
|
472
|
+
// SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
|
|
472
473
|
return Object.assign(tag, {
|
|
473
474
|
id,
|
|
474
475
|
layer: makeLayer,
|
package/src/Hyperdrive.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, type Layer } from "effect";
|
|
1
|
+
import { Context, Effect, type Layer, Predicate } from "effect";
|
|
2
2
|
|
|
3
3
|
import * as Binding from "./Binding";
|
|
4
4
|
import type { WorkerEnvironment } from "./Environment";
|
|
@@ -45,15 +45,8 @@ export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
|
|
|
45
45
|
>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export const isHyperdrive = (value:
|
|
49
|
-
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const resource = value as Record<string, unknown>;
|
|
54
|
-
|
|
55
|
-
return typeof resource.connectionString === "string";
|
|
56
|
-
};
|
|
48
|
+
export const isHyperdrive = <Candidate>(value: Candidate): value is Candidate & Hyperdrive =>
|
|
49
|
+
Predicate.hasProperty(value, "connectionString") && Predicate.isString(value.connectionString);
|
|
57
50
|
|
|
58
51
|
export const makeClient =
|
|
59
52
|
(definition: HyperdriveDefinition) =>
|
|
@@ -80,6 +73,7 @@ export const Tag =
|
|
|
80
73
|
|
|
81
74
|
const makeLayer = (definition: LayerOptions) => layer(tag, definition);
|
|
82
75
|
|
|
76
|
+
// SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
|
|
83
77
|
return Object.assign(tag, {
|
|
84
78
|
id,
|
|
85
79
|
layer: makeLayer,
|
package/src/Images.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
ImageUploadOptions as CloudflareImageUploadOptions,
|
|
17
17
|
Response as CloudflareResponse,
|
|
18
18
|
} from "@cloudflare/workers-types";
|
|
19
|
-
import { Context, Data, Effect, Function, Option, type Layer } from "effect";
|
|
19
|
+
import { Context, Data, Effect, Function, Option, Predicate, type Layer } from "effect";
|
|
20
20
|
|
|
21
21
|
import * as Binding from "./Binding";
|
|
22
22
|
import type { WorkerEnvironment } from "./Environment";
|
|
@@ -224,21 +224,18 @@ const tryImagesSync = <A>(
|
|
|
224
224
|
const maybe = <A>(value: A | null): Option.Option<A> =>
|
|
225
225
|
value === null ? Option.none() : Option.some(value);
|
|
226
226
|
|
|
227
|
-
const hasFunction = (value:
|
|
228
|
-
|
|
227
|
+
const hasFunction = <Candidate>(value: Candidate, key: string): boolean =>
|
|
228
|
+
Predicate.hasProperty(value, key) && Predicate.isFunction(value[key]);
|
|
229
229
|
|
|
230
|
-
const isHostedImagesBinding = (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
hasFunction(value, "image") &&
|
|
234
|
-
hasFunction(value, "upload") &&
|
|
235
|
-
hasFunction(value, "list");
|
|
230
|
+
const isHostedImagesBinding = <Candidate>(
|
|
231
|
+
value: Candidate,
|
|
232
|
+
): value is Candidate & CloudflareHostedImagesBinding =>
|
|
233
|
+
hasFunction(value, "image") && hasFunction(value, "upload") && hasFunction(value, "list");
|
|
236
234
|
|
|
237
|
-
export const isImagesBinding = (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
hasFunction(value, "info") &&
|
|
241
|
-
hasFunction(value, "input");
|
|
235
|
+
export const isImagesBinding = <Candidate>(
|
|
236
|
+
value: Candidate,
|
|
237
|
+
): value is Candidate & ImagesRuntimeBinding =>
|
|
238
|
+
hasFunction(value, "info") && hasFunction(value, "input");
|
|
242
239
|
|
|
243
240
|
const wrapResult = (
|
|
244
241
|
binding: string,
|
|
@@ -342,6 +339,7 @@ export const Tag =
|
|
|
342
339
|
|
|
343
340
|
const makeLayer = (definition: LayerOptions) => layer(tag, definition);
|
|
344
341
|
|
|
342
|
+
// SAFETY: these are exactly the members required by TagClass, attached to the matching service tag.
|
|
345
343
|
return Object.assign(tag, {
|
|
346
344
|
id,
|
|
347
345
|
layer: makeLayer,
|
package/src/Kv.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
KVNamespaceListOptions as CloudflareKVNamespaceListOptions,
|
|
3
3
|
KVNamespacePutOptions as CloudflareKVNamespacePutOptions,
|
|
4
4
|
} from "@cloudflare/workers-types";
|
|
5
|
-
import { Context, Data, Effect, Option, Schema as S, type Layer } from "effect";
|
|
5
|
+
import { Context, Data, Effect, Option, Predicate, Schema as S, type Layer } from "effect";
|
|
6
6
|
|
|
7
7
|
import * as Binding from "./Binding";
|
|
8
8
|
import type { WorkerEnvironment } from "./Environment";
|
|
@@ -144,21 +144,17 @@ const tryKvPromise = <A>(
|
|
|
144
144
|
catch: (cause) => kvError(binding, operation, cause),
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
export const isKvNamespace = (value:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
typeof resource.getWithMetadata === "function" &&
|
|
159
|
-
typeof resource.list === "function"
|
|
160
|
-
);
|
|
161
|
-
};
|
|
147
|
+
export const isKvNamespace = <Candidate>(value: Candidate): value is Candidate & KVNamespace =>
|
|
148
|
+
Predicate.hasProperty(value, "get") &&
|
|
149
|
+
Predicate.isFunction(value.get) &&
|
|
150
|
+
Predicate.hasProperty(value, "put") &&
|
|
151
|
+
Predicate.isFunction(value.put) &&
|
|
152
|
+
Predicate.hasProperty(value, "delete") &&
|
|
153
|
+
Predicate.isFunction(value.delete) &&
|
|
154
|
+
Predicate.hasProperty(value, "getWithMetadata") &&
|
|
155
|
+
Predicate.isFunction(value.getWithMetadata) &&
|
|
156
|
+
Predicate.hasProperty(value, "list") &&
|
|
157
|
+
Predicate.isFunction(value.list);
|
|
162
158
|
|
|
163
159
|
export const makeClient = <Key, Value, EncodedValue>(
|
|
164
160
|
definition: KvDefinition<Key, Value, EncodedValue>,
|
|
@@ -232,7 +228,8 @@ export const makeClient = <Key, Value, EncodedValue>(
|
|
|
232
228
|
key.metadata === undefined
|
|
233
229
|
? Option.none<Metadata>()
|
|
234
230
|
: metadataSchema === undefined
|
|
235
|
-
?
|
|
231
|
+
? // SAFETY: without a metadata schema, KV's generic Metadata contract is passed through.
|
|
232
|
+
Option.some(key.metadata as Metadata)
|
|
236
233
|
: Option.some(yield* S.decodeUnknownEffect(metadataSchema)(key.metadata));
|
|
237
234
|
|
|
238
235
|
keys.push({
|
|
@@ -310,6 +307,7 @@ export const Tag =
|
|
|
310
307
|
|
|
311
308
|
const makeLayer = (binding: LayerOptions) => kvDefinition.layer(tag, binding);
|
|
312
309
|
|
|
310
|
+
// SAFETY: the assigned schemas and layer exactly implement TagClass for this KV definition.
|
|
313
311
|
return Object.assign(tag, {
|
|
314
312
|
id: kvDefinition.id,
|
|
315
313
|
keySchema: kvDefinition.key,
|
package/src/Queue.ts
CHANGED
|
@@ -38,6 +38,7 @@ export class QueueMessageDecodeError extends Data.TaggedError("QueueMessageDecod
|
|
|
38
38
|
type RuntimeContext<ROut> = ExecutionContext | WorkerContext | WorkerEnvironment | ROut;
|
|
39
39
|
|
|
40
40
|
type QueueHandlerContext<ROut> = RuntimeContext<ROut> | Scope.Scope;
|
|
41
|
+
type EncodedQueueBody = globalThis.Message<unknown>["body"];
|
|
41
42
|
|
|
42
43
|
export type QueueHandler<ROut, Body = unknown> = (
|
|
43
44
|
batch: QueueBatch<Body>,
|
|
@@ -71,7 +72,7 @@ export const fromMessageBatch = <Body>(
|
|
|
71
72
|
|
|
72
73
|
export const decodeBatch = <Body>(
|
|
73
74
|
batch: globalThis.MessageBatch<unknown>,
|
|
74
|
-
decodeBody: (body:
|
|
75
|
+
decodeBody: (body: EncodedQueueBody) => Effect.Effect<Body, unknown>,
|
|
75
76
|
): Effect.Effect<QueueBatch<Body>, QueueMessageDecodeError> =>
|
|
76
77
|
Effect.gen(function* () {
|
|
77
78
|
const messages: Array<QueueMessage<Body>> = [];
|
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 { type Context, Data, Effect, Schema as S } from "effect";
|
|
10
|
+
import { type Context, Data, Effect, Predicate, Schema as S } from "effect";
|
|
11
11
|
|
|
12
12
|
import * as Binding from "./Binding";
|
|
13
13
|
import type * as RpcDefinition from "./RpcDefinition";
|
|
@@ -24,6 +24,8 @@ export type QueueProducer<Body> = Pick<CloudflareQueue<Body>, "send"> &
|
|
|
24
24
|
|
|
25
25
|
const expectedQueueProducer = "Queue producer binding with send(); optional sendBatch()/metrics()";
|
|
26
26
|
|
|
27
|
+
type QueueCandidate = Parameters<typeof Predicate.isUnknown>[0];
|
|
28
|
+
|
|
27
29
|
export interface QueueBindingDefinition<Message extends RpcDefinition.ServiceFreeSchema> {
|
|
28
30
|
/** Binding name as configured in `wrangler.jsonc`. */
|
|
29
31
|
readonly binding: string;
|
|
@@ -41,7 +43,7 @@ export interface QueueBindingClient<Message extends RpcDefinition.ServiceFreeSch
|
|
|
41
43
|
options?: QueueSendBatchOptions,
|
|
42
44
|
) => Effect.Effect<void, QueueOperationError | S.SchemaError>;
|
|
43
45
|
readonly metrics: () => Effect.Effect<QueueMetrics, QueueOperationError>;
|
|
44
|
-
readonly rawUnsafe: Effect.Effect<
|
|
46
|
+
readonly rawUnsafe: Effect.Effect<QueueProducer<S.Codec.Encoded<Message>>>;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export class QueueOperationError extends Data.TaggedError("QueueOperationError")<{
|
|
@@ -67,15 +69,8 @@ const tryQueuePromise = <A>(
|
|
|
67
69
|
}),
|
|
68
70
|
});
|
|
69
71
|
|
|
70
|
-
export const isQueue = <Body>(value:
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const resource = value as Record<string, unknown>;
|
|
76
|
-
|
|
77
|
-
return typeof resource.send === "function";
|
|
78
|
-
};
|
|
72
|
+
export const isQueue = <Body>(value: QueueCandidate): value is QueueProducer<Body> =>
|
|
73
|
+
Predicate.hasProperty(value, "send") && Predicate.isFunction(value.send);
|
|
79
74
|
|
|
80
75
|
export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
|
|
81
76
|
definition: QueueBindingDefinition<Message>,
|
|
@@ -136,7 +131,7 @@ export const makeClient = <Message extends RpcDefinition.ServiceFreeSchema>(
|
|
|
136
131
|
|
|
137
132
|
return tryQueuePromise(definition.binding, "metrics", () => metrics.call(queue));
|
|
138
133
|
},
|
|
139
|
-
rawUnsafe: Effect.succeed(queue
|
|
134
|
+
rawUnsafe: Effect.succeed(queue),
|
|
140
135
|
});
|
|
141
136
|
};
|
|
142
137
|
|