effect-cf 0.23.0 → 0.24.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 +14 -0
- package/README.md +151 -0
- package/dist/{Hyperdrive-COyg7R26.d.mts → Binding-Bq_FPzlt.d.mts} +4 -36
- package/dist/DurableObjectState-8sgBJB0s.mjs +384 -0
- package/dist/Hyperdrive-pEMnh3W6.d.mts +36 -0
- package/dist/HyperdrivePg.d.mts +2 -1
- package/dist/Vitest.d.mts +192 -0
- package/dist/Vitest.mjs +184 -0
- package/dist/WorkflowBinding-BECGNTzu.d.mts +1081 -0
- package/dist/index.d.mts +80 -1153
- package/dist/index.mjs +3 -383
- package/package.json +10 -1
- package/src/Vitest.ts +500 -0
- package/src/WorkflowBinding.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# effect-cf
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#82](https://github.com/danieljvdm/effect-cf/pull/82) [`877f289`](https://github.com/danieljvdm/effect-cf/commit/877f289a19ff15bdca1e7f9320aee3be1cffb3de) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add an `effect-cf/vitest` entrypoint with Effect-native Workers environment, fetch, queue, execution-context, and scoped Workflow testing helpers for Cloudflare's Vitest Workers pool.
|
|
8
|
+
|
|
9
|
+
- [#84](https://github.com/danieljvdm/effect-cf/pull/84) [`9bbd554`](https://github.com/danieljvdm/effect-cf/commit/9bbd5543ca463447d0eb6a08d6441c7b72086b40) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add Effect-native scheduled and Pages runners, Durable Object test controls, D1 migrations, binding reset helpers, Workflow introspectors, and a Secrets Store admin client to `effect-cf/vitest`. The optional `@cloudflare/vitest-pool-workers` peer dependency now requires `^0.21.3`.
|
|
10
|
+
|
|
11
|
+
## 0.23.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#80](https://github.com/danieljvdm/effect-cf/pull/80) [`cd83c7c`](https://github.com/danieljvdm/effect-cf/commit/cd83c7cc86bb8edfcd1449c17cca1c26aecc8a06) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Stop failing workflow status reads with `WorkflowResultDecodeError` when Cloudflare reports `output: null` for a non-complete instance. The wrapped `WorkflowInstance.status` now returns the real status (e.g. `errored`) with `Option.none()` output and the preserved `error`, while a completed workflow with a `Schema.Null` result still decodes `null` as `Option.some(null)`.
|
|
16
|
+
|
|
3
17
|
## 0.23.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -43,6 +43,157 @@ Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
|
|
|
43
43
|
- `Workflow` - typed Workflow entrypoints, steps, starter clients, and instance types
|
|
44
44
|
- `Rpc` - Cloudflare RPC type helpers and scoped disposal utilities
|
|
45
45
|
- `WorkerConfig` - Effect `Config` helpers backed by Cloudflare `env`
|
|
46
|
+
- `effect-cf/vitest` - Effect-native runners and scoped test helpers for Cloudflare's Vitest Workers pool
|
|
47
|
+
|
|
48
|
+
## Vitest Workers Pool
|
|
49
|
+
|
|
50
|
+
Install and configure a `^0.21.3` release of
|
|
51
|
+
[`@cloudflare/vitest-pool-workers`](https://github.com/cloudflare/workers-sdk/tree/main/packages/vitest-pool-workers#readme),
|
|
52
|
+
then import the test-only helpers from `effect-cf/vitest`. The `fetch` runner
|
|
53
|
+
constructs the Worker with the current test environment and waits for all
|
|
54
|
+
`waitUntil` work before completing the Effect.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { assert, it } from "@effect/vitest";
|
|
58
|
+
import { Config, Effect } from "effect";
|
|
59
|
+
import * as PoolWorkers from "effect-cf/vitest";
|
|
60
|
+
|
|
61
|
+
import WorkerEntrypoint from "../src/index";
|
|
62
|
+
|
|
63
|
+
it.effect("serves a request", () =>
|
|
64
|
+
Effect.gen(function* () {
|
|
65
|
+
const response = yield* PoolWorkers.fetch(
|
|
66
|
+
WorkerEntrypoint,
|
|
67
|
+
new Request("https://worker.test/health"),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
assert.strictEqual(response.status, 200);
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
it.effect("reads Wrangler vars", () =>
|
|
75
|
+
Config.string("APP_NAME").pipe(
|
|
76
|
+
Effect.tap((appName) => assert.strictEqual(appName, "test-app")),
|
|
77
|
+
Effect.provide(PoolWorkers.layer),
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Queue consumers can be invoked with typed message bodies while retaining the
|
|
83
|
+
pool's acknowledgement and retry result:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
it.effect("acknowledges a job", () =>
|
|
87
|
+
Effect.gen(function* () {
|
|
88
|
+
const { result } = yield* PoolWorkers.queue(MyQueueConsumer, "jobs", [
|
|
89
|
+
{
|
|
90
|
+
id: "job-1",
|
|
91
|
+
timestamp: new Date(),
|
|
92
|
+
attempts: 1,
|
|
93
|
+
body: { accountId: "account-1" },
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
assert.deepStrictEqual(result.explicitAcks, ["job-1"]);
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Scheduled handlers and Pages Functions use the same lifecycle behavior: their
|
|
103
|
+
Effects complete only after the event's `waitUntil` work has settled. Pages
|
|
104
|
+
tests must configure the `ASSETS` binding required by Pool Workers.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
it.effect("runs the cron handler", () =>
|
|
108
|
+
PoolWorkers.scheduled(worker.scheduled, {
|
|
109
|
+
cron: "30 * * * *",
|
|
110
|
+
scheduledTime: new Date("2030-01-01T00:00:00Z"),
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it.effect("runs a Pages Function", () =>
|
|
115
|
+
Effect.gen(function* () {
|
|
116
|
+
const response = yield* PoolWorkers.pages(onRequest, {
|
|
117
|
+
request: new Request("https://pages.test/users/dan"),
|
|
118
|
+
params: { user: "dan" },
|
|
119
|
+
data: { authenticated: true },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.strictEqual(response.status, 200);
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`runInDurableObject` carries the test's Effect context into the Durable Object
|
|
128
|
+
I/O context and provides the same wrapped `DurableObjectState` service used by
|
|
129
|
+
production handlers. Typed failures remain in the Effect error channel.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
it.effect("seeds a Durable Object", () =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
const stub = env.COUNTERS.getByName("home");
|
|
135
|
+
|
|
136
|
+
yield* PoolWorkers.runInDurableObject(stub, () =>
|
|
137
|
+
Effect.gen(function* () {
|
|
138
|
+
const state = yield* DurableObjectState;
|
|
139
|
+
yield* state.storage.put("count", 41);
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
assert.strictEqual(yield* PoolWorkers.runDurableObjectAlarm(stub), true);
|
|
144
|
+
|
|
145
|
+
// Recreate in-memory state while preserving Durable Object storage.
|
|
146
|
+
yield* PoolWorkers.evictDurableObject(stub);
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The subpath also exposes Effects for `listDurableObjectIds`, `reset`,
|
|
152
|
+
`abortAllDurableObjects`, `evictAllDurableObjects`, and `applyD1Migrations`.
|
|
153
|
+
`adminSecretsStore` returns an Effect-native admin client whose `create`,
|
|
154
|
+
`update`, `duplicate`, `delete`, `list`, and `get` operations are Effects.
|
|
155
|
+
|
|
156
|
+
`withExecutionContext` supports lower-level tests that need a native
|
|
157
|
+
`ExecutionContext`. Workflow introspectors and all of their modifiers and query
|
|
158
|
+
operations are Effect-native. They are disposed with the current Effect scope,
|
|
159
|
+
and modifier callbacks carry the caller's Effect context and typed failures.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
it.effect("controls a Workflow instance", () =>
|
|
163
|
+
Effect.scoped(
|
|
164
|
+
Effect.gen(function* () {
|
|
165
|
+
const instanceId = crypto.randomUUID();
|
|
166
|
+
const instance = yield* PoolWorkers.introspectWorkflowInstance(
|
|
167
|
+
env.REPORT_WORKFLOW,
|
|
168
|
+
instanceId,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
yield* instance.modify((modifier) =>
|
|
172
|
+
Effect.gen(function* () {
|
|
173
|
+
yield* modifier.disableSleeps();
|
|
174
|
+
yield* modifier.mockStepResult({ name: "render-report" }, "reports/test.json");
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
yield* Effect.promise(() =>
|
|
178
|
+
env.REPORT_WORKFLOW.create({
|
|
179
|
+
id: instanceId,
|
|
180
|
+
params: { reportId: "test", requestedBy: "dan@example.com" },
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
yield* instance.waitForStatus("complete");
|
|
185
|
+
assert.deepStrictEqual(yield* instance.getOutput, {
|
|
186
|
+
objectKey: "reports/test.json",
|
|
187
|
+
notified: true,
|
|
188
|
+
});
|
|
189
|
+
}),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use `introspectWorkflow` when the instance ID is not known before creation. Its
|
|
195
|
+
`modifyAll` and `get` operations are Effects, and each instance returned by
|
|
196
|
+
`get` uses the same Effect-native interface shown above.
|
|
46
197
|
|
|
47
198
|
## Worker Example
|
|
48
199
|
|
|
@@ -88,7 +88,7 @@ declare namespace WorkerConfig {
|
|
|
88
88
|
const layerWith: (makeProvider: (env: WorkerEnv) => ConfigProvider.ConfigProvider) => Layer.Layer<never, never, WorkerEnvironment>;
|
|
89
89
|
}
|
|
90
90
|
declare namespace Binding_d_exports {
|
|
91
|
-
export { BindingNotFoundError, BindingService, BindingValidationError, Service, TypeId, ValidationOptions, layer
|
|
91
|
+
export { BindingNotFoundError, BindingService, BindingValidationError, Service, TypeId, ValidationOptions, layer };
|
|
92
92
|
}
|
|
93
93
|
/** Internal type id marker used by binding helper services. */
|
|
94
94
|
declare const TypeId: "~effect-cf/Binding";
|
|
@@ -127,43 +127,11 @@ interface BindingService<Self, Id extends string, Service> extends Context.Servi
|
|
|
127
127
|
readonly binding: string;
|
|
128
128
|
readonly layer: Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
129
129
|
}
|
|
130
|
-
declare function layer
|
|
131
|
-
declare function layer
|
|
130
|
+
declare function layer<Self, Resource>(tag: Context.Service<Self, Resource>, binding: string, isResource: (value: unknown) => value is Resource, wrap?: undefined, options?: ValidationOptions): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
131
|
+
declare function layer<Self, Resource, Service>(tag: Context.Service<Self, Service>, binding: string, isResource: (value: unknown) => value is Resource, wrap: (resource: Resource) => Service, options?: ValidationOptions): Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
132
132
|
declare const Service: <Self>() => {
|
|
133
133
|
<Id extends string, Resource>(id: Id, binding: string, isResource: (value: unknown) => value is Resource, wrap?: undefined, options?: ValidationOptions): BindingService<Self, Id, Resource>;
|
|
134
134
|
<Id extends string, Resource, Service>(id: Id, binding: string, isResource: (value: unknown) => value is Resource, wrap: (resource: Resource) => Service, options?: ValidationOptions): BindingService<Self, Id, Service>;
|
|
135
135
|
};
|
|
136
|
-
declare namespace Hyperdrive_d_exports {
|
|
137
|
-
export { HyperdriveClient, HyperdriveDefinition, HyperdriveService, LayerOptions, Tag, TagClass, isHyperdrive, layer, make, makeClient };
|
|
138
|
-
}
|
|
139
|
-
/** Typed Hyperdrive binding definition. */
|
|
140
|
-
interface HyperdriveDefinition {
|
|
141
|
-
/** Binding name as configured in `wrangler.jsonc`. */
|
|
142
|
-
readonly binding: string;
|
|
143
|
-
}
|
|
144
|
-
interface HyperdriveClient {
|
|
145
|
-
readonly connectionString: string;
|
|
146
|
-
readonly rawUnsafe: Effect.Effect<Hyperdrive>;
|
|
147
|
-
readonly definition: HyperdriveDefinition;
|
|
148
|
-
}
|
|
149
|
-
declare const HyperdriveServiceTypeId: unique symbol;
|
|
150
|
-
/** Nominal service marker for Hyperdrive services created with {@link make}. */
|
|
151
|
-
interface HyperdriveService<Id extends string> {
|
|
152
|
-
readonly [HyperdriveServiceTypeId]: {
|
|
153
|
-
readonly id: Id;
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
type LayerOptions = {
|
|
157
|
-
readonly binding: string;
|
|
158
|
-
};
|
|
159
|
-
interface TagClass<Self, Id extends string> extends Context.ServiceClass<Self, Id, HyperdriveClient> {
|
|
160
|
-
readonly id: Id;
|
|
161
|
-
readonly layer: (options: LayerOptions) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
162
|
-
}
|
|
163
|
-
declare const isHyperdrive: (value: unknown) => value is Hyperdrive;
|
|
164
|
-
declare const makeClient: (definition: HyperdriveDefinition) => (hyperdrive: Hyperdrive) => HyperdriveClient;
|
|
165
|
-
declare const layer: <Self>(tag: Context.Service<Self, HyperdriveClient>, definition: HyperdriveDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
166
|
-
declare const make: <Id extends string>(id: Id) => TagClass<HyperdriveService<Id>, Id>;
|
|
167
|
-
declare const Tag: <Self>() => <Id extends string>(id: Id) => TagClass<Self, Id>;
|
|
168
136
|
//#endregion
|
|
169
|
-
export {
|
|
137
|
+
export { Environment_d_exports as a, WorkerEnvironment as c, Binding_d_exports as i, BindingService as n, WorkerConfig as o, BindingValidationError as r, WorkerEnv as s, BindingNotFoundError as t };
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { Cause, Config, ConfigProvider, Context, Data, Effect, Exit, Option, Result, Schema } from "effect";
|
|
3
|
+
//#region src/Environment.ts
|
|
4
|
+
var Environment_exports = /* @__PURE__ */ __exportAll({
|
|
5
|
+
WorkerConfig: () => WorkerConfig,
|
|
6
|
+
WorkerEnvironment: () => WorkerEnvironment
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Context service for reading worker bindings from the current `env` object.
|
|
10
|
+
*/
|
|
11
|
+
var WorkerEnvironment = class extends Context.Service()("effect-cf/WorkerEnvironment") {};
|
|
12
|
+
let WorkerConfig;
|
|
13
|
+
(function(_WorkerConfig) {
|
|
14
|
+
_WorkerConfig.string = (name) => Config.string(name);
|
|
15
|
+
_WorkerConfig.redacted = (name) => Config.redacted(name);
|
|
16
|
+
_WorkerConfig.number = (name) => Config.number(name);
|
|
17
|
+
_WorkerConfig.integer = (name) => Config.int(name);
|
|
18
|
+
_WorkerConfig.boolean = (name) => Config.boolean(name);
|
|
19
|
+
const providerFromEnv = _WorkerConfig.providerFromEnv = (env, options) => ConfigProvider.fromUnknown(env, options);
|
|
20
|
+
const providerWith = _WorkerConfig.providerWith = (makeProvider) => Effect.map(WorkerEnvironment, makeProvider);
|
|
21
|
+
const provider = _WorkerConfig.provider = providerWith(providerFromEnv);
|
|
22
|
+
_WorkerConfig.layer = _WorkerConfig.providerLayer = ConfigProvider.layer(provider);
|
|
23
|
+
_WorkerConfig.layerWith = (makeProvider) => ConfigProvider.layer(providerWith(makeProvider));
|
|
24
|
+
})(WorkerConfig || (WorkerConfig = {}));
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/internal/ErrorMessage.ts
|
|
27
|
+
/**
|
|
28
|
+
* Renders the most useful human-readable description of an unknown `cause`
|
|
29
|
+
* for use in tagged error `message` getters.
|
|
30
|
+
*/
|
|
31
|
+
const causeMessage = (cause) => {
|
|
32
|
+
if (typeof cause === "object" && cause !== null) {
|
|
33
|
+
const message = Reflect.get(cause, "message");
|
|
34
|
+
if (typeof message === "string" && message.length > 0) return message;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return String(cause);
|
|
38
|
+
} catch {
|
|
39
|
+
return "unknown cause";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** Renders a violation list as a single `path: message; ...` line. */
|
|
43
|
+
const violationsMessage = (violations) => violations.map((violation) => `${violation.path}: ${violation.message}`).join("; ");
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/DurableObjectStorage.ts
|
|
46
|
+
var DurableObjectStorage_exports = /* @__PURE__ */ __exportAll({
|
|
47
|
+
StorageOperationError: () => StorageOperationError,
|
|
48
|
+
fromDurableObjectStorage: () => fromDurableObjectStorage
|
|
49
|
+
});
|
|
50
|
+
/** Error type used when a storage operation throws or rejects. */
|
|
51
|
+
var StorageOperationError = class extends Data.TaggedError("StorageOperationError") {
|
|
52
|
+
get message() {
|
|
53
|
+
return `Durable Object storage ${this.operation} failed: ${causeMessage(this.cause)}`;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const storageError = (operation, cause) => new StorageOperationError({
|
|
57
|
+
operation,
|
|
58
|
+
cause
|
|
59
|
+
});
|
|
60
|
+
const tryStorageSync = (operation, evaluate) => Effect.try({
|
|
61
|
+
try: evaluate,
|
|
62
|
+
catch: (cause) => storageError(operation, cause)
|
|
63
|
+
});
|
|
64
|
+
const tryStoragePromise = (operation, evaluate) => Effect.tryPromise({
|
|
65
|
+
try: evaluate,
|
|
66
|
+
catch: (cause) => storageError(operation, cause)
|
|
67
|
+
});
|
|
68
|
+
const fromSqlCursor = (cursor) => ({
|
|
69
|
+
next: () => tryStorageSync("sql.next", () => cursor.next()),
|
|
70
|
+
toArray: () => tryStorageSync("sql.toArray", () => cursor.toArray()),
|
|
71
|
+
one: () => tryStorageSync("sql.one", () => cursor.one()),
|
|
72
|
+
raw: () => tryStorageSync("sql.raw", () => cursor.raw()),
|
|
73
|
+
get columnNames() {
|
|
74
|
+
return cursor.columnNames;
|
|
75
|
+
},
|
|
76
|
+
rowsRead: tryStorageSync("sql.rowsRead", () => cursor.rowsRead),
|
|
77
|
+
rowsWritten: tryStorageSync("sql.rowsWritten", () => cursor.rowsWritten)
|
|
78
|
+
});
|
|
79
|
+
const fromSqlStorage = (sql) => ({
|
|
80
|
+
exec: (query, ...bindings) => tryStorageSync("sql.exec", () => fromSqlCursor(sql.exec(query, ...bindings))),
|
|
81
|
+
get databaseSize() {
|
|
82
|
+
return sql.databaseSize;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const schemaBackedSyncKvStorage = (kv, definition) => {
|
|
86
|
+
const encodeKey = Schema.encodeEffect(definition.key);
|
|
87
|
+
const decodeKey = Schema.decodeUnknownEffect(definition.key);
|
|
88
|
+
const encodeValue = Schema.encodeEffect(definition.value);
|
|
89
|
+
const decodeValue = Schema.decodeUnknownEffect(definition.value);
|
|
90
|
+
return {
|
|
91
|
+
get: (key) => Effect.gen(function* () {
|
|
92
|
+
const encodedKey = yield* encodeKey(key);
|
|
93
|
+
const encodedValue = yield* tryStorageSync("kv.get", () => kv.get(encodedKey));
|
|
94
|
+
if (encodedValue === void 0) return Option.none();
|
|
95
|
+
return yield* decodeValue(encodedValue).pipe(Effect.map(Option.some));
|
|
96
|
+
}),
|
|
97
|
+
put: (key, value) => Effect.gen(function* () {
|
|
98
|
+
const encodedKey = yield* encodeKey(key);
|
|
99
|
+
const encodedValue = yield* encodeValue(value);
|
|
100
|
+
yield* tryStorageSync("kv.put", () => kv.put(encodedKey, encodedValue));
|
|
101
|
+
}),
|
|
102
|
+
delete: (key) => Effect.gen(function* () {
|
|
103
|
+
const encodedKey = yield* encodeKey(key);
|
|
104
|
+
return yield* tryStorageSync("kv.delete", () => kv.delete(encodedKey));
|
|
105
|
+
}),
|
|
106
|
+
list: (options) => Effect.gen(function* () {
|
|
107
|
+
const entries = Array.from(yield* tryStorageSync("kv.list", () => kv.list(options)));
|
|
108
|
+
const decoded = [];
|
|
109
|
+
for (const [key, value] of entries) decoded.push([yield* decodeKey(key), yield* decodeValue(value)]);
|
|
110
|
+
return decoded;
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const fromSyncKvStorage = (kv) => ({
|
|
115
|
+
get: (key) => tryStorageSync("kv.get", () => kv.get(key)),
|
|
116
|
+
put: (key, value) => tryStorageSync("kv.put", () => kv.put(key, value)),
|
|
117
|
+
delete: (key) => tryStorageSync("kv.delete", () => kv.delete(key)),
|
|
118
|
+
list: (options) => tryStorageSync("kv.list", () => Array.from(kv.list(options))),
|
|
119
|
+
schema: (definition) => schemaBackedSyncKvStorage(kv, definition)
|
|
120
|
+
});
|
|
121
|
+
const fromDurableObjectTransaction = (txn) => ({
|
|
122
|
+
get: (keyOrKeys, options) => Array.isArray(keyOrKeys) ? tryStoragePromise("transaction.get", () => txn.get(keyOrKeys, options)) : tryStoragePromise("transaction.get", () => txn.get(keyOrKeys, options)),
|
|
123
|
+
list: (options) => tryStoragePromise("transaction.list", () => txn.list(options)),
|
|
124
|
+
put: (keyOrEntries, valueOrOptions, maybeOptions) => tryStoragePromise("transaction.put", () => typeof keyOrEntries === "string" ? txn.put(keyOrEntries, valueOrOptions, maybeOptions) : txn.put(keyOrEntries, valueOrOptions)),
|
|
125
|
+
delete: (keyOrKeys, options) => Array.isArray(keyOrKeys) ? tryStoragePromise("transaction.delete", () => txn.delete(keyOrKeys, options)) : tryStoragePromise("transaction.delete", () => txn.delete(keyOrKeys, options)),
|
|
126
|
+
rollback: () => tryStorageSync("transaction.rollback", () => txn.rollback()),
|
|
127
|
+
getAlarm: (options) => tryStoragePromise("transaction.getAlarm", () => txn.getAlarm(options)),
|
|
128
|
+
setAlarm: (scheduledTime, options) => tryStoragePromise("transaction.setAlarm", () => txn.setAlarm(scheduledTime, options)),
|
|
129
|
+
deleteAlarm: (options) => tryStoragePromise("transaction.deleteAlarm", () => txn.deleteAlarm(options))
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* Wraps native Cloudflare storage APIs as Effect-returning helpers.
|
|
133
|
+
*/
|
|
134
|
+
const fromDurableObjectStorage = (storage) => ({
|
|
135
|
+
get: (key, options) => tryStoragePromise("get", () => storage.get(key, options)),
|
|
136
|
+
put: (key, value, options) => tryStoragePromise("put", () => storage.put(key, value, options)),
|
|
137
|
+
delete: (key, options) => tryStoragePromise("delete", () => storage.delete(key, options)),
|
|
138
|
+
deleteAll: (options) => tryStoragePromise("deleteAll", () => storage.deleteAll(options)),
|
|
139
|
+
getAlarm: (options) => tryStoragePromise("getAlarm", () => storage.getAlarm(options)),
|
|
140
|
+
setAlarm: (scheduledTime, options) => tryStoragePromise("setAlarm", () => storage.setAlarm(scheduledTime, options)),
|
|
141
|
+
deleteAlarm: (options) => tryStoragePromise("deleteAlarm", () => storage.deleteAlarm(options)),
|
|
142
|
+
transactionSync: (closure) => Effect.context().pipe(Effect.flatMap((context) => Effect.suspend(() => {
|
|
143
|
+
try {
|
|
144
|
+
return Effect.succeed(storage.transactionSync(() => {
|
|
145
|
+
const exit = Effect.runSyncExitWith(context)(closure());
|
|
146
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
147
|
+
throw exit;
|
|
148
|
+
}));
|
|
149
|
+
} catch (cause) {
|
|
150
|
+
if (Exit.isExit(cause) && Exit.isFailure(cause)) return Effect.failCause(cause.cause);
|
|
151
|
+
return Effect.fail(storageError("transactionSync", cause));
|
|
152
|
+
}
|
|
153
|
+
}))),
|
|
154
|
+
transaction: (closure) => Effect.context().pipe(Effect.flatMap((context) => Effect.callback((resume) => {
|
|
155
|
+
storage.transaction(async (txn) => {
|
|
156
|
+
const exit = await Effect.runPromiseExitWith(context)(closure(fromDurableObjectTransaction(txn)));
|
|
157
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
158
|
+
throw exit;
|
|
159
|
+
}).then((value) => resume(Effect.succeed(value)), (cause) => {
|
|
160
|
+
if (Exit.isExit(cause) && Exit.isFailure(cause)) resume(Effect.failCause(cause.cause));
|
|
161
|
+
else resume(Effect.fail(storageError("transaction", cause)));
|
|
162
|
+
});
|
|
163
|
+
}))),
|
|
164
|
+
sync: () => tryStoragePromise("sync", () => storage.sync()),
|
|
165
|
+
getCurrentBookmark: () => tryStoragePromise("getCurrentBookmark", () => storage.getCurrentBookmark()),
|
|
166
|
+
onNextSessionRestoreBookmark: (bookmark) => tryStoragePromise("onNextSessionRestoreBookmark", () => storage.onNextSessionRestoreBookmark(bookmark)),
|
|
167
|
+
sql: fromSqlStorage(storage.sql),
|
|
168
|
+
kv: fromSyncKvStorage(storage.kv)
|
|
169
|
+
});
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/DurableObjectWebSocket.ts
|
|
172
|
+
var DurableObjectWebSocket_exports = /* @__PURE__ */ __exportAll({
|
|
173
|
+
DurableWebSocketAttachmentError: () => DurableWebSocketAttachmentError,
|
|
174
|
+
DurableWebSocketCloseError: () => DurableWebSocketCloseError,
|
|
175
|
+
DurableWebSocketSendError: () => DurableWebSocketSendError,
|
|
176
|
+
acceptUpgrade: () => acceptUpgrade,
|
|
177
|
+
attachment: () => attachment,
|
|
178
|
+
fromWebSocket: () => fromWebSocket,
|
|
179
|
+
handlers: () => handlers
|
|
180
|
+
});
|
|
181
|
+
/** Error raised when sending on a Durable Object websocket fails. */
|
|
182
|
+
var DurableWebSocketSendError = class extends Data.TaggedError("DurableWebSocketSendError") {
|
|
183
|
+
get message() {
|
|
184
|
+
return `Durable Object websocket send failed: ${causeMessage(this.cause)}`;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
/** Error raised when closing a Durable Object websocket fails. */
|
|
188
|
+
var DurableWebSocketCloseError = class extends Data.TaggedError("DurableWebSocketCloseError") {
|
|
189
|
+
get message() {
|
|
190
|
+
return `Durable Object websocket close failed: ${causeMessage(this.cause)}`;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
/** Error raised when serializing or deserializing a websocket attachment fails. */
|
|
194
|
+
var DurableWebSocketAttachmentError = class extends Data.TaggedError("DurableWebSocketAttachmentError") {
|
|
195
|
+
get message() {
|
|
196
|
+
return `Durable Object websocket attachment ${this.operation} failed: ${causeMessage(this.cause)}`;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const wrappers = /* @__PURE__ */ new WeakMap();
|
|
200
|
+
/** Wraps a native Cloudflare websocket in the Effect-native Durable Object API. */
|
|
201
|
+
const fromWebSocket = (raw) => {
|
|
202
|
+
const existing = wrappers.get(raw);
|
|
203
|
+
if (existing !== void 0) return existing;
|
|
204
|
+
const socket = {
|
|
205
|
+
raw,
|
|
206
|
+
send: (data) => Effect.try({
|
|
207
|
+
try: () => raw.send(data),
|
|
208
|
+
catch: (cause) => new DurableWebSocketSendError({ cause })
|
|
209
|
+
}),
|
|
210
|
+
close: (code, reason) => Effect.try({
|
|
211
|
+
try: () => raw.close(code, reason),
|
|
212
|
+
catch: (cause) => new DurableWebSocketCloseError({ cause })
|
|
213
|
+
}),
|
|
214
|
+
serializeAttachment: (value) => Effect.try({
|
|
215
|
+
try: () => raw.serializeAttachment(value),
|
|
216
|
+
catch: (cause) => new DurableWebSocketAttachmentError({
|
|
217
|
+
operation: "serialize",
|
|
218
|
+
cause
|
|
219
|
+
})
|
|
220
|
+
}),
|
|
221
|
+
deserializeAttachment: Effect.try({
|
|
222
|
+
try: () => raw.deserializeAttachment(),
|
|
223
|
+
catch: (cause) => new DurableWebSocketAttachmentError({
|
|
224
|
+
operation: "deserialize",
|
|
225
|
+
cause
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
wrappers.set(raw, socket);
|
|
230
|
+
return socket;
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Accepts a websocket upgrade and registers the server socket on `DurableObjectState`.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* const response = yield* DurableObjectWebSocket.acceptUpgrade({
|
|
238
|
+
* tags: ["room:general"],
|
|
239
|
+
* });
|
|
240
|
+
*
|
|
241
|
+
* return response.response;
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
const acceptUpgrade = Effect.fn("DurableObjectWebSocket.acceptUpgrade")(function* (options = {}) {
|
|
245
|
+
const state = yield* DurableObjectState;
|
|
246
|
+
const pair = new WebSocketPair();
|
|
247
|
+
const client = pair[0];
|
|
248
|
+
const server = fromWebSocket(pair[1]);
|
|
249
|
+
if (options.attachment !== void 0) yield* server.serializeAttachment(options.attachment);
|
|
250
|
+
yield* state.acceptWebSocket(server, options.tags === void 0 ? void 0 : [...options.tags]);
|
|
251
|
+
return {
|
|
252
|
+
client,
|
|
253
|
+
server,
|
|
254
|
+
response: new Response(null, {
|
|
255
|
+
status: 101,
|
|
256
|
+
webSocket: client
|
|
257
|
+
})
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
/** Creates typed attachment helpers for accepted and rehydrated sockets. */
|
|
261
|
+
const attachment = (schema) => {
|
|
262
|
+
const serialize = (socket, value) => Schema.encodeEffect(schema)(value).pipe(Effect.mapError((cause) => new DurableWebSocketAttachmentError({
|
|
263
|
+
operation: "serialize",
|
|
264
|
+
cause
|
|
265
|
+
})), Effect.flatMap((encoded) => socket.serializeAttachment(encoded)));
|
|
266
|
+
const deserialize = (socket) => Effect.gen(function* () {
|
|
267
|
+
const value = yield* socket.deserializeAttachment;
|
|
268
|
+
if (value === null || value === void 0) return Option.none();
|
|
269
|
+
return Option.some(yield* Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new DurableWebSocketAttachmentError({
|
|
270
|
+
operation: "deserialize",
|
|
271
|
+
cause
|
|
272
|
+
}))));
|
|
273
|
+
});
|
|
274
|
+
const rehydrate = (options = {}) => Effect.gen(function* () {
|
|
275
|
+
const sockets = yield* (yield* DurableObjectState).getWebSockets(options.tag);
|
|
276
|
+
const restored = [];
|
|
277
|
+
const onInvalid = options.onInvalid ?? "ignore";
|
|
278
|
+
for (const socket of sockets) {
|
|
279
|
+
const decoded = yield* Effect.result(deserialize(socket));
|
|
280
|
+
if (Result.isSuccess(decoded) && Option.isSome(decoded.success)) {
|
|
281
|
+
restored.push({
|
|
282
|
+
socket,
|
|
283
|
+
attachment: decoded.success.value
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (Result.isFailure(decoded) && onInvalid === "fail") return yield* Effect.fail(decoded.failure);
|
|
288
|
+
if (Result.isFailure(decoded) && onInvalid === "ignore-and-close") yield* socket.close(1008, "invalid websocket attachment").pipe(Effect.ignore);
|
|
289
|
+
}
|
|
290
|
+
return restored;
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
serialize,
|
|
294
|
+
deserialize,
|
|
295
|
+
rehydrate,
|
|
296
|
+
schema
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
/** Maps compact websocket lifecycle handler names to `DurableObject.make` options. */
|
|
300
|
+
const handlers = (options) => ({
|
|
301
|
+
webSocketMessage: options.message,
|
|
302
|
+
webSocketClose: options.close,
|
|
303
|
+
webSocketError: options.error
|
|
304
|
+
});
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/internal/WorkerContext.ts
|
|
307
|
+
const causeError = (cause) => {
|
|
308
|
+
const squashed = Cause.squash(cause);
|
|
309
|
+
return squashed instanceof Error ? squashed : new Error(Cause.pretty(cause), { cause: squashed });
|
|
310
|
+
};
|
|
311
|
+
const failureHandler = (label, cause, options) => (options?.onFailure?.(cause) ?? Effect.logError(`${label} failed`, Cause.pretty(cause))).pipe(Effect.catchCause((handlerCause) => Effect.logError(`${label} failure handler failed`, Cause.pretty(cause), Cause.pretty(handlerCause))));
|
|
312
|
+
/**
|
|
313
|
+
* Builds a `waitUntil` scheduler for any Cloudflare lifecycle that accepts
|
|
314
|
+
* background promises (`ExecutionContext`, `DurableObjectState`).
|
|
315
|
+
*
|
|
316
|
+
* Background effects capture the calling fiber's context via `Effect.context`
|
|
317
|
+
* before being registered, so the default module-level `Effect.runPromiseExit`
|
|
318
|
+
* runner is sufficient; entrypoints may still pass a runtime-bound runner.
|
|
319
|
+
*/
|
|
320
|
+
const makeWaitUntilScheduler = (label, register, runPromiseExit = (effect) => Effect.runPromiseExit(effect)) => {
|
|
321
|
+
return (effect, options, mode) => Effect.context().pipe(Effect.flatMap((context) => Effect.sync(() => {
|
|
322
|
+
const runHandler = (cause) => runPromiseExit(Effect.scoped(Effect.provideContext(failureHandler(label, cause, options), context))).then((exit) => {
|
|
323
|
+
if (Exit.isFailure(exit)) console.error(`${label} failure handler failed`, Cause.pretty(exit.cause));
|
|
324
|
+
});
|
|
325
|
+
register(runPromiseExit(Effect.scoped(Effect.provideContext(effect, context))).then(async (exit) => {
|
|
326
|
+
if (Exit.isSuccess(exit)) return;
|
|
327
|
+
await runHandler(exit.cause);
|
|
328
|
+
if (mode === "propagate") throw causeError(exit.cause);
|
|
329
|
+
}));
|
|
330
|
+
})));
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* Builds the `WorkerContext` service from a native `ExecutionContext`.
|
|
334
|
+
*/
|
|
335
|
+
const fromExecutionContext = (ctx, runPromiseExit = (effect) => Effect.runPromiseExit(effect)) => {
|
|
336
|
+
const schedule = makeWaitUntilScheduler("WorkerContext.waitUntil", (promise) => ctx.waitUntil(promise), runPromiseExit);
|
|
337
|
+
return {
|
|
338
|
+
raw: ctx,
|
|
339
|
+
waitUntil: (effect, options) => schedule(effect, options, options?.mode ?? "observe"),
|
|
340
|
+
waitUntilPropagating: (effect, options) => schedule(effect, options, "propagate"),
|
|
341
|
+
passThroughOnException: Effect.sync(() => ctx.passThroughOnException())
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/DurableObjectState.ts
|
|
346
|
+
var DurableObjectState_exports = /* @__PURE__ */ __exportAll({
|
|
347
|
+
DurableObjectState: () => DurableObjectState,
|
|
348
|
+
fromDurableObjectState: () => fromDurableObjectState
|
|
349
|
+
});
|
|
350
|
+
/**
|
|
351
|
+
* Context tag for accessing the current Durable Object state service.
|
|
352
|
+
*/
|
|
353
|
+
var DurableObjectState = class extends Context.Service()("effect-cf/DurableObjectState") {};
|
|
354
|
+
/**
|
|
355
|
+
* Wraps a native Cloudflare `DurableObjectState` as a {@link DurableObjectStateService}.
|
|
356
|
+
*/
|
|
357
|
+
const fromDurableObjectState = (state) => {
|
|
358
|
+
const scheduleWaitUntil = makeWaitUntilScheduler("DurableObjectState.waitUntil", (promise) => state.waitUntil(promise));
|
|
359
|
+
const waitUntil = ((input, options) => Effect.isEffect(input) ? scheduleWaitUntil(input, options, options?.mode ?? "observe") : Effect.sync(() => state.waitUntil(input)));
|
|
360
|
+
return {
|
|
361
|
+
raw: state,
|
|
362
|
+
id: state.id,
|
|
363
|
+
storage: fromDurableObjectStorage(state.storage),
|
|
364
|
+
waitUntil,
|
|
365
|
+
blockConcurrencyWhile: (effect) => Effect.context().pipe(Effect.flatMap((context) => Effect.promise(() => state.blockConcurrencyWhile(() => runPromiseExitPreservingTypedFailures(context, effect)))), Effect.flatten),
|
|
366
|
+
blockConcurrencyWhileOrReset: (effect) => Effect.context().pipe(Effect.flatMap((context) => Effect.promise(() => state.blockConcurrencyWhile(() => Effect.runPromiseWith(context)(effect))))),
|
|
367
|
+
acceptWebSocket: (ws, tags) => Effect.sync(() => state.acceptWebSocket(ws.raw, tags)),
|
|
368
|
+
getWebSockets: (tag) => Effect.sync(() => state.getWebSockets(tag).map((socket) => fromWebSocket(socket))),
|
|
369
|
+
setWebSocketAutoResponse: (pair) => Effect.sync(() => state.setWebSocketAutoResponse(pair)),
|
|
370
|
+
getWebSocketAutoResponse: Effect.sync(() => state.getWebSocketAutoResponse()),
|
|
371
|
+
getWebSocketAutoResponseTimestamp: (ws) => Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws.raw)),
|
|
372
|
+
setHibernatableWebSocketEventTimeout: (timeoutMs) => Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
|
|
373
|
+
getHibernatableWebSocketEventTimeout: Effect.sync(() => state.getHibernatableWebSocketEventTimeout()),
|
|
374
|
+
getTags: (ws) => Effect.sync(() => state.getTags(ws.raw)),
|
|
375
|
+
abort: (reason) => Effect.sync(() => state.abort(reason))
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
const runPromiseExitPreservingTypedFailures = async (context, effect) => {
|
|
379
|
+
const exit = await Effect.runPromiseExitWith(context)(effect);
|
|
380
|
+
if (Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause))) throw Cause.squash(exit.cause);
|
|
381
|
+
return exit;
|
|
382
|
+
};
|
|
383
|
+
//#endregion
|
|
384
|
+
export { DurableObjectWebSocket_exports as a, causeMessage as c, WorkerConfig as d, WorkerEnvironment as f, fromExecutionContext as i, violationsMessage as l, DurableObjectState_exports as n, fromWebSocket as o, fromDurableObjectState as r, DurableObjectStorage_exports as s, DurableObjectState as t, Environment_exports as u };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-Bq_FPzlt.mjs";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
declare namespace Hyperdrive_d_exports {
|
|
4
|
+
export { HyperdriveClient, HyperdriveDefinition, HyperdriveService, LayerOptions, Tag, TagClass, isHyperdrive, layer, make, makeClient };
|
|
5
|
+
}
|
|
6
|
+
/** Typed Hyperdrive binding definition. */
|
|
7
|
+
interface HyperdriveDefinition {
|
|
8
|
+
/** Binding name as configured in `wrangler.jsonc`. */
|
|
9
|
+
readonly binding: string;
|
|
10
|
+
}
|
|
11
|
+
interface HyperdriveClient {
|
|
12
|
+
readonly connectionString: string;
|
|
13
|
+
readonly rawUnsafe: Effect.Effect<Hyperdrive>;
|
|
14
|
+
readonly definition: HyperdriveDefinition;
|
|
15
|
+
}
|
|
16
|
+
declare const HyperdriveServiceTypeId: unique symbol;
|
|
17
|
+
/** Nominal service marker for Hyperdrive services created with {@link make}. */
|
|
18
|
+
interface HyperdriveService<Id extends string> {
|
|
19
|
+
readonly [HyperdriveServiceTypeId]: {
|
|
20
|
+
readonly id: Id;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
type LayerOptions = {
|
|
24
|
+
readonly binding: string;
|
|
25
|
+
};
|
|
26
|
+
interface TagClass<Self, Id extends string> extends Context.ServiceClass<Self, Id, HyperdriveClient> {
|
|
27
|
+
readonly id: Id;
|
|
28
|
+
readonly layer: (options: LayerOptions) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
29
|
+
}
|
|
30
|
+
declare const isHyperdrive: (value: unknown) => value is Hyperdrive;
|
|
31
|
+
declare const makeClient: (definition: HyperdriveDefinition) => (hyperdrive: Hyperdrive) => HyperdriveClient;
|
|
32
|
+
declare const layer: <Self>(tag: Context.Service<Self, HyperdriveClient>, definition: HyperdriveDefinition) => Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
|
|
33
|
+
declare const make: <Id extends string>(id: Id) => TagClass<HyperdriveService<Id>, Id>;
|
|
34
|
+
declare const Tag: <Self>() => <Id extends string>(id: Id) => TagClass<Self, Id>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { LayerOptions as n, TagClass as r, Hyperdrive_d_exports as t };
|
package/dist/HyperdrivePg.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as WorkerEnvironment, r as BindingValidationError, t as BindingNotFoundError } from "./Binding-Bq_FPzlt.mjs";
|
|
2
|
+
import { n as LayerOptions, r as TagClass } from "./Hyperdrive-pEMnh3W6.mjs";
|
|
2
3
|
import { Layer } from "effect";
|
|
3
4
|
import { PgClient } from "@effect/sql-pg";
|
|
4
5
|
import { SqlClient, SqlError } from "effect/unstable/sql";
|