effect-cf 0.0.1
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/README.md +79 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/index.d.mts +1362 -0
- package/dist/index.mjs +1198 -0
- package/package.json +47 -0
- package/src/Binding.ts +70 -0
- package/src/DurableObject.ts +220 -0
- package/src/DurableObjectDefinition.ts +214 -0
- package/src/DurableObjectNamespace.ts +483 -0
- package/src/DurableObjectRpcWebSocket.ts +222 -0
- package/src/DurableObjectState.ts +127 -0
- package/src/DurableObjectStorage.ts +381 -0
- package/src/DurableObjectWebSocket.ts +62 -0
- package/src/Entry.ts +259 -0
- package/src/Environment.ts +86 -0
- package/src/Kv.ts +244 -0
- package/src/Rpc.ts +127 -0
- package/src/RpcDefinition.ts +354 -0
- package/src/ServiceBinding.ts +278 -0
- package/src/Worker.ts +264 -0
- package/src/WorkerDefinition.ts +206 -0
- package/src/index.ts +15 -0
- package/src/internal/Entrypoint.ts +40 -0
- package/src/internal/RpcInvocation.ts +73 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Cause, Context, Effect, Exit } from "effect";
|
|
2
|
+
|
|
3
|
+
import { fromDurableObjectStorage, type DurableObjectStorage } from "./DurableObjectStorage";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Effect-friendly wrapper around Cloudflare `DurableObjectState`.
|
|
7
|
+
*/
|
|
8
|
+
export interface DurableObjectStateService {
|
|
9
|
+
/** Underlying Cloudflare state instance. */
|
|
10
|
+
readonly raw: globalThis.DurableObjectState;
|
|
11
|
+
/** Durable Object id for the current instance. */
|
|
12
|
+
readonly id: globalThis.DurableObjectId;
|
|
13
|
+
/** Wrapped storage API. */
|
|
14
|
+
readonly storage: DurableObjectStorage;
|
|
15
|
+
/** Registers background work with Cloudflare's lifecycle. */
|
|
16
|
+
waitUntil(promise: Promise<unknown>): Effect.Effect<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Runs an Effect inside Cloudflare's `blockConcurrencyWhile` gate.
|
|
19
|
+
*
|
|
20
|
+
* Cloudflare resets a Durable Object if the callback throws/rejects, or if the
|
|
21
|
+
* callback exceeds the platform timeout (currently documented as 30 seconds).
|
|
22
|
+
* This safe default resolves the callback with the Effect `Exit` for typed
|
|
23
|
+
* Effect failures and resumes that `Exit` afterward, so typed failures remain
|
|
24
|
+
* ordinary Effect failures instead of resetting the Durable Object. Defects
|
|
25
|
+
* and interruptions still reject the callback.
|
|
26
|
+
*/
|
|
27
|
+
blockConcurrencyWhile<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
|
|
28
|
+
/**
|
|
29
|
+
* Runs an Effect inside Cloudflare's `blockConcurrencyWhile` gate and lets
|
|
30
|
+
* Effect failures reject the callback.
|
|
31
|
+
*
|
|
32
|
+
* Use this only when Cloudflare's reset-on-throw behavior is intentional. The
|
|
33
|
+
* platform also resets the Durable Object if the callback exceeds the
|
|
34
|
+
* documented timeout (currently 30 seconds).
|
|
35
|
+
*/
|
|
36
|
+
blockConcurrencyWhileOrReset<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>;
|
|
37
|
+
/** Accepts a websocket connection for hibernation-capable Durable Objects. */
|
|
38
|
+
acceptWebSocket(ws: WebSocket, tags?: Array<string>): Effect.Effect<void>;
|
|
39
|
+
/** Lists active sockets, optionally filtered by tag. */
|
|
40
|
+
getWebSockets(tag?: string): Effect.Effect<Array<WebSocket>>;
|
|
41
|
+
/** Configures automatic request/response handling for sockets. */
|
|
42
|
+
setWebSocketAutoResponse(pair?: WebSocketRequestResponsePair): Effect.Effect<void>;
|
|
43
|
+
/** Gets the configured websocket auto-response pair. */
|
|
44
|
+
getWebSocketAutoResponse: Effect.Effect<WebSocketRequestResponsePair | null>;
|
|
45
|
+
/** Timestamp of the last automatic websocket response for a socket. */
|
|
46
|
+
getWebSocketAutoResponseTimestamp(ws: WebSocket): Effect.Effect<Date | null>;
|
|
47
|
+
/** Sets the timeout for hibernatable websocket events. */
|
|
48
|
+
setHibernatableWebSocketEventTimeout(timeoutMs?: number): Effect.Effect<void>;
|
|
49
|
+
/** Gets the timeout for hibernatable websocket events. */
|
|
50
|
+
getHibernatableWebSocketEventTimeout: Effect.Effect<number | null>;
|
|
51
|
+
/** Reads tags attached to a websocket. */
|
|
52
|
+
getTags(ws: WebSocket): Effect.Effect<Array<string>>;
|
|
53
|
+
/**
|
|
54
|
+
* Forcibly resets the Durable Object. Cloudflare logs an uncaught Error using
|
|
55
|
+
* the optional reason, and the method is not available in `wrangler dev` local
|
|
56
|
+
* development according to the Durable Object State docs.
|
|
57
|
+
*/
|
|
58
|
+
abort(reason?: string): Effect.Effect<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Context tag for accessing the current Durable Object state service.
|
|
63
|
+
*/
|
|
64
|
+
export class DurableObjectState extends Context.Service<
|
|
65
|
+
DurableObjectState,
|
|
66
|
+
DurableObjectStateService
|
|
67
|
+
>()("effect-cf/DurableObjectState") {}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wraps a native Cloudflare `DurableObjectState` as a {@link DurableObjectStateService}.
|
|
71
|
+
*/
|
|
72
|
+
export const fromDurableObjectState = (
|
|
73
|
+
state: globalThis.DurableObjectState,
|
|
74
|
+
): DurableObjectStateService => ({
|
|
75
|
+
raw: state,
|
|
76
|
+
id: state.id,
|
|
77
|
+
storage: fromDurableObjectStorage(state.storage),
|
|
78
|
+
waitUntil: (promise: Promise<unknown>) => Effect.sync(() => state.waitUntil(promise)),
|
|
79
|
+
blockConcurrencyWhile: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
80
|
+
Effect.context<R>().pipe(
|
|
81
|
+
Effect.flatMap((context) =>
|
|
82
|
+
Effect.promise(() =>
|
|
83
|
+
state.blockConcurrencyWhile(() =>
|
|
84
|
+
runPromiseExitPreservingTypedFailures(Effect.provideContext(effect, context)),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
Effect.flatten,
|
|
89
|
+
),
|
|
90
|
+
blockConcurrencyWhileOrReset: <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
91
|
+
Effect.context<R>().pipe(
|
|
92
|
+
Effect.flatMap((context) =>
|
|
93
|
+
Effect.promise(() =>
|
|
94
|
+
state.blockConcurrencyWhile(() =>
|
|
95
|
+
Effect.runPromise(Effect.provideContext(effect, context)),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
acceptWebSocket: (ws: WebSocket, tags?: Array<string>) =>
|
|
101
|
+
Effect.sync(() => state.acceptWebSocket(ws, tags)),
|
|
102
|
+
getWebSockets: (tag?: string) => Effect.sync(() => state.getWebSockets(tag)),
|
|
103
|
+
setWebSocketAutoResponse: (pair?: WebSocketRequestResponsePair) =>
|
|
104
|
+
Effect.sync(() => state.setWebSocketAutoResponse(pair)),
|
|
105
|
+
getWebSocketAutoResponse: Effect.sync(() => state.getWebSocketAutoResponse()),
|
|
106
|
+
getWebSocketAutoResponseTimestamp: (ws: WebSocket) =>
|
|
107
|
+
Effect.sync(() => state.getWebSocketAutoResponseTimestamp(ws)),
|
|
108
|
+
setHibernatableWebSocketEventTimeout: (timeoutMs?: number) =>
|
|
109
|
+
Effect.sync(() => state.setHibernatableWebSocketEventTimeout(timeoutMs)),
|
|
110
|
+
getHibernatableWebSocketEventTimeout: Effect.sync(() =>
|
|
111
|
+
state.getHibernatableWebSocketEventTimeout(),
|
|
112
|
+
),
|
|
113
|
+
getTags: (ws: WebSocket) => Effect.sync(() => state.getTags(ws)),
|
|
114
|
+
abort: (reason?: string) => Effect.sync(() => state.abort(reason)),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const runPromiseExitPreservingTypedFailures = async <A, E>(
|
|
118
|
+
effect: Effect.Effect<A, E>,
|
|
119
|
+
): Promise<Exit.Exit<A, E>> => {
|
|
120
|
+
const exit = await Effect.runPromiseExit(effect);
|
|
121
|
+
|
|
122
|
+
if (Exit.isFailure(exit) && (Cause.hasDies(exit.cause) || Cause.hasInterrupts(exit.cause))) {
|
|
123
|
+
throw Cause.squash(exit.cause);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return exit;
|
|
127
|
+
};
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { Data, Effect, Exit, Option, Schema as S } from "effect";
|
|
2
|
+
|
|
3
|
+
/** Supported primitive value types for Durable Object SQL APIs. */
|
|
4
|
+
export type SqlStorageValue = globalThis.SqlStorageValue;
|
|
5
|
+
|
|
6
|
+
/** Error type used when a storage operation throws or rejects. */
|
|
7
|
+
export class StorageOperationError extends Data.TaggedError("StorageOperationError")<{
|
|
8
|
+
readonly operation: string;
|
|
9
|
+
readonly cause: unknown;
|
|
10
|
+
}> {}
|
|
11
|
+
|
|
12
|
+
type StorageEffect<A> = Effect.Effect<A, StorageOperationError>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Effect wrapper for Cloudflare SQL cursor operations.
|
|
16
|
+
*/
|
|
17
|
+
export interface SqlCursor<T extends Record<string, SqlStorageValue>> {
|
|
18
|
+
next(): StorageEffect<{ done?: false; value: T } | { done: true; value?: never }>;
|
|
19
|
+
toArray(): StorageEffect<Array<T>>;
|
|
20
|
+
one(): StorageEffect<T>;
|
|
21
|
+
raw<U extends Array<SqlStorageValue>>(): StorageEffect<IterableIterator<U>>;
|
|
22
|
+
readonly columnNames: Array<string>;
|
|
23
|
+
readonly rowsRead: StorageEffect<number>;
|
|
24
|
+
readonly rowsWritten: StorageEffect<number>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Effect wrapper for Durable Object SQLite APIs.
|
|
29
|
+
*/
|
|
30
|
+
export interface SqlStorage {
|
|
31
|
+
/** Executes a SQL statement and returns a typed cursor. */
|
|
32
|
+
exec<T extends Record<string, SqlStorageValue>>(
|
|
33
|
+
query: string,
|
|
34
|
+
...bindings: Array<SqlStorageValue>
|
|
35
|
+
): StorageEffect<SqlCursor<T>>;
|
|
36
|
+
readonly databaseSize: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Schema pair used by `SyncKvStorage.schema(...)`.
|
|
41
|
+
*/
|
|
42
|
+
export interface SyncKvDefinition<Key, Value, EncodedValue> {
|
|
43
|
+
readonly key: S.Codec<Key, string>;
|
|
44
|
+
readonly value: S.Codec<Value, EncodedValue>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sync KV wrapper that transparently encodes keys/values through Effect Schema.
|
|
49
|
+
*/
|
|
50
|
+
export interface SchemaBackedSyncKvStorage<Key, Value> {
|
|
51
|
+
get(key: Key): Effect.Effect<Option.Option<Value>, unknown>;
|
|
52
|
+
put(key: Key, value: Value): Effect.Effect<void, unknown>;
|
|
53
|
+
delete(key: Key): Effect.Effect<boolean, unknown>;
|
|
54
|
+
list(options?: globalThis.SyncKvListOptions): Effect.Effect<Array<[Key, Value]>, unknown>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Effect wrapper for synchronous KV attached to Durable Object SQLite storage.
|
|
59
|
+
*/
|
|
60
|
+
export interface SyncKvStorage {
|
|
61
|
+
get<T = unknown>(key: string): StorageEffect<T | undefined>;
|
|
62
|
+
put<T>(key: string, value: T): StorageEffect<void>;
|
|
63
|
+
delete(key: string): StorageEffect<boolean>;
|
|
64
|
+
list<T = unknown>(options?: globalThis.SyncKvListOptions): StorageEffect<Array<[string, T]>>;
|
|
65
|
+
schema<Key, Value, EncodedValue>(
|
|
66
|
+
definition: SyncKvDefinition<Key, Value, EncodedValue>,
|
|
67
|
+
): SchemaBackedSyncKvStorage<Key, Value>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Effect wrapper around Cloudflare transaction callbacks.
|
|
72
|
+
*/
|
|
73
|
+
export interface DurableObjectTransaction {
|
|
74
|
+
get<T = unknown>(
|
|
75
|
+
key: string,
|
|
76
|
+
options?: globalThis.DurableObjectGetOptions,
|
|
77
|
+
): StorageEffect<T | undefined>;
|
|
78
|
+
get<T = unknown>(
|
|
79
|
+
keys: Array<string>,
|
|
80
|
+
options?: globalThis.DurableObjectGetOptions,
|
|
81
|
+
): StorageEffect<Map<string, T>>;
|
|
82
|
+
list<T = unknown>(options?: globalThis.DurableObjectListOptions): StorageEffect<Map<string, T>>;
|
|
83
|
+
put<T>(key: string, value: T, options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
|
|
84
|
+
put<T>(
|
|
85
|
+
entries: Record<string, T>,
|
|
86
|
+
options?: globalThis.DurableObjectPutOptions,
|
|
87
|
+
): StorageEffect<void>;
|
|
88
|
+
delete(key: string, options?: globalThis.DurableObjectPutOptions): StorageEffect<boolean>;
|
|
89
|
+
delete(keys: Array<string>, options?: globalThis.DurableObjectPutOptions): StorageEffect<number>;
|
|
90
|
+
rollback(): StorageEffect<void>;
|
|
91
|
+
getAlarm(options?: globalThis.DurableObjectGetAlarmOptions): StorageEffect<number | null>;
|
|
92
|
+
setAlarm(
|
|
93
|
+
scheduledTime: number | Date,
|
|
94
|
+
options?: globalThis.DurableObjectSetAlarmOptions,
|
|
95
|
+
): StorageEffect<void>;
|
|
96
|
+
deleteAlarm(options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Effect wrapper around Cloudflare Durable Object storage.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const program = Effect.gen(function* () {
|
|
105
|
+
* const state = yield* DurableObjectState;
|
|
106
|
+
* yield* state.storage.put("counter", 1);
|
|
107
|
+
* const value = yield* state.storage.get<number>("counter");
|
|
108
|
+
* return value;
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export interface DurableObjectStorage {
|
|
113
|
+
get<T = unknown>(
|
|
114
|
+
key: string,
|
|
115
|
+
options?: globalThis.DurableObjectGetOptions,
|
|
116
|
+
): StorageEffect<T | undefined>;
|
|
117
|
+
put<T>(key: string, value: T, options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
|
|
118
|
+
delete(key: string, options?: globalThis.DurableObjectPutOptions): StorageEffect<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* Deletes all stored data. On compatibility dates before 2026-02-24, Cloudflare
|
|
121
|
+
* documents that active alarms must be deleted separately with `deleteAlarm()`.
|
|
122
|
+
*/
|
|
123
|
+
deleteAll(options?: globalThis.DurableObjectPutOptions): StorageEffect<void>;
|
|
124
|
+
getAlarm(options?: globalThis.DurableObjectGetAlarmOptions): StorageEffect<number | null>;
|
|
125
|
+
setAlarm(
|
|
126
|
+
scheduledTime: number | Date,
|
|
127
|
+
options?: globalThis.DurableObjectSetAlarmOptions,
|
|
128
|
+
): StorageEffect<void>;
|
|
129
|
+
deleteAlarm(options?: globalThis.DurableObjectSetAlarmOptions): StorageEffect<void>;
|
|
130
|
+
/**
|
|
131
|
+
* SQLite-backed Durable Object storage only.
|
|
132
|
+
*
|
|
133
|
+
* The callback Effect must complete synchronously. If it requires asynchronous
|
|
134
|
+
* work, use `transaction` instead so Cloudflare can run the async transaction
|
|
135
|
+
* callback under the platform's transaction contract.
|
|
136
|
+
*/
|
|
137
|
+
transactionSync<A, E, R>(
|
|
138
|
+
closure: () => Effect.Effect<A, E, R>,
|
|
139
|
+
): Effect.Effect<A, E | StorageOperationError, R>;
|
|
140
|
+
/**
|
|
141
|
+
* Runs an async transaction and exposes a typed transaction wrapper.
|
|
142
|
+
*/
|
|
143
|
+
transaction<A, E, R>(
|
|
144
|
+
closure: (txn: DurableObjectTransaction) => Effect.Effect<A, E, R>,
|
|
145
|
+
): Effect.Effect<A, E | StorageOperationError, R>;
|
|
146
|
+
/** Flushes pending writes to disk. */
|
|
147
|
+
sync(): StorageEffect<void>;
|
|
148
|
+
/** SQLite-backed Durable Object storage point-in-time recovery API only. */
|
|
149
|
+
getCurrentBookmark(): StorageEffect<string>;
|
|
150
|
+
/** SQLite-backed Durable Object storage point-in-time recovery API only. */
|
|
151
|
+
onNextSessionRestoreBookmark(bookmark: string): StorageEffect<string>;
|
|
152
|
+
readonly sql: SqlStorage;
|
|
153
|
+
readonly kv: SyncKvStorage;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const storageError = (operation: string, cause: unknown) =>
|
|
157
|
+
new StorageOperationError({ operation, cause });
|
|
158
|
+
|
|
159
|
+
const tryStorageSync = <A>(operation: string, evaluate: () => A): StorageEffect<A> =>
|
|
160
|
+
Effect.try({
|
|
161
|
+
try: evaluate,
|
|
162
|
+
catch: (cause) => storageError(operation, cause),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const tryStoragePromise = <A>(operation: string, evaluate: () => Promise<A>): StorageEffect<A> =>
|
|
166
|
+
Effect.tryPromise({
|
|
167
|
+
try: evaluate,
|
|
168
|
+
catch: (cause) => storageError(operation, cause),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const fromSqlCursor = <T extends Record<string, SqlStorageValue>>(
|
|
172
|
+
cursor: globalThis.SqlStorageCursor<T>,
|
|
173
|
+
): SqlCursor<T> => ({
|
|
174
|
+
next: () => tryStorageSync("sql.next", () => cursor.next()),
|
|
175
|
+
toArray: () => tryStorageSync("sql.toArray", () => cursor.toArray()),
|
|
176
|
+
one: () => tryStorageSync("sql.one", () => cursor.one()),
|
|
177
|
+
raw: <U extends Array<SqlStorageValue>>() => tryStorageSync("sql.raw", () => cursor.raw<U>()),
|
|
178
|
+
get columnNames() {
|
|
179
|
+
return cursor.columnNames;
|
|
180
|
+
},
|
|
181
|
+
rowsRead: tryStorageSync("sql.rowsRead", () => cursor.rowsRead),
|
|
182
|
+
rowsWritten: tryStorageSync("sql.rowsWritten", () => cursor.rowsWritten),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const fromSqlStorage = (sql: globalThis.SqlStorage): SqlStorage => ({
|
|
186
|
+
exec: <T extends Record<string, SqlStorageValue>>(
|
|
187
|
+
query: string,
|
|
188
|
+
...bindings: Array<SqlStorageValue>
|
|
189
|
+
) => tryStorageSync("sql.exec", () => fromSqlCursor(sql.exec<T>(query, ...bindings))),
|
|
190
|
+
get databaseSize() {
|
|
191
|
+
return sql.databaseSize;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const schemaBackedSyncKvStorage = <Key, Value, EncodedValue>(
|
|
196
|
+
kv: globalThis.SyncKvStorage,
|
|
197
|
+
definition: SyncKvDefinition<Key, Value, EncodedValue>,
|
|
198
|
+
): SchemaBackedSyncKvStorage<Key, Value> => {
|
|
199
|
+
const encodeKey = S.encodeEffect(definition.key);
|
|
200
|
+
const decodeKey = S.decodeUnknownEffect(definition.key);
|
|
201
|
+
const encodeValue = S.encodeEffect(definition.value);
|
|
202
|
+
const decodeValue = S.decodeUnknownEffect(definition.value);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
get: (key) =>
|
|
206
|
+
Effect.gen(function* () {
|
|
207
|
+
const encodedKey = yield* encodeKey(key);
|
|
208
|
+
const encodedValue = yield* tryStorageSync("kv.get", () =>
|
|
209
|
+
kv.get<EncodedValue>(encodedKey),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (encodedValue === undefined) {
|
|
213
|
+
return Option.none();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return yield* decodeValue(encodedValue).pipe(Effect.map(Option.some));
|
|
217
|
+
}),
|
|
218
|
+
put: (key, value) =>
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const encodedKey = yield* encodeKey(key);
|
|
221
|
+
const encodedValue = yield* encodeValue(value);
|
|
222
|
+
yield* tryStorageSync("kv.put", () => kv.put(encodedKey, encodedValue));
|
|
223
|
+
}),
|
|
224
|
+
delete: (key) =>
|
|
225
|
+
Effect.gen(function* () {
|
|
226
|
+
const encodedKey = yield* encodeKey(key);
|
|
227
|
+
return yield* tryStorageSync("kv.delete", () => kv.delete(encodedKey));
|
|
228
|
+
}),
|
|
229
|
+
list: (options) =>
|
|
230
|
+
Effect.gen(function* () {
|
|
231
|
+
const entries = Array.from(
|
|
232
|
+
yield* tryStorageSync("kv.list", () => kv.list<EncodedValue>(options)),
|
|
233
|
+
);
|
|
234
|
+
const decoded: Array<[Key, Value]> = [];
|
|
235
|
+
|
|
236
|
+
for (const [key, value] of entries) {
|
|
237
|
+
decoded.push([yield* decodeKey(key), yield* decodeValue(value)]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return decoded;
|
|
241
|
+
}),
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const fromSyncKvStorage = (kv: globalThis.SyncKvStorage): SyncKvStorage => ({
|
|
246
|
+
get: <T = unknown>(key: string) => tryStorageSync("kv.get", () => kv.get<T>(key)),
|
|
247
|
+
put: <T>(key: string, value: T) => tryStorageSync("kv.put", () => kv.put(key, value)),
|
|
248
|
+
delete: (key: string) => tryStorageSync("kv.delete", () => kv.delete(key)),
|
|
249
|
+
list: <T = unknown>(options?: globalThis.SyncKvListOptions) =>
|
|
250
|
+
tryStorageSync("kv.list", () => Array.from(kv.list<T>(options))),
|
|
251
|
+
schema: <Key, Value, EncodedValue>(definition: SyncKvDefinition<Key, Value, EncodedValue>) =>
|
|
252
|
+
schemaBackedSyncKvStorage(kv, definition),
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const fromDurableObjectTransaction = (
|
|
256
|
+
txn: globalThis.DurableObjectTransaction,
|
|
257
|
+
): DurableObjectTransaction =>
|
|
258
|
+
({
|
|
259
|
+
get: <T = unknown>(
|
|
260
|
+
keyOrKeys: string | Array<string>,
|
|
261
|
+
options?: globalThis.DurableObjectGetOptions,
|
|
262
|
+
) =>
|
|
263
|
+
Array.isArray(keyOrKeys)
|
|
264
|
+
? tryStoragePromise("transaction.get", () => txn.get<T>(keyOrKeys, options))
|
|
265
|
+
: tryStoragePromise("transaction.get", () => txn.get<T>(keyOrKeys, options)),
|
|
266
|
+
list: <T = unknown>(options?: globalThis.DurableObjectListOptions) =>
|
|
267
|
+
tryStoragePromise("transaction.list", () => txn.list<T>(options)),
|
|
268
|
+
put: <T>(
|
|
269
|
+
keyOrEntries: string | Record<string, T>,
|
|
270
|
+
valueOrOptions?: T | globalThis.DurableObjectPutOptions,
|
|
271
|
+
maybeOptions?: globalThis.DurableObjectPutOptions,
|
|
272
|
+
) =>
|
|
273
|
+
tryStoragePromise("transaction.put", () =>
|
|
274
|
+
typeof keyOrEntries === "string"
|
|
275
|
+
? txn.put(keyOrEntries, valueOrOptions as T, maybeOptions)
|
|
276
|
+
: txn.put(keyOrEntries, valueOrOptions as globalThis.DurableObjectPutOptions | undefined),
|
|
277
|
+
),
|
|
278
|
+
delete: (keyOrKeys: string | Array<string>, options?: globalThis.DurableObjectPutOptions) =>
|
|
279
|
+
Array.isArray(keyOrKeys)
|
|
280
|
+
? tryStoragePromise("transaction.delete", () => txn.delete(keyOrKeys, options))
|
|
281
|
+
: tryStoragePromise("transaction.delete", () => txn.delete(keyOrKeys, options)),
|
|
282
|
+
rollback: () => tryStorageSync("transaction.rollback", () => txn.rollback()),
|
|
283
|
+
getAlarm: (options?: globalThis.DurableObjectGetAlarmOptions) =>
|
|
284
|
+
tryStoragePromise("transaction.getAlarm", () => txn.getAlarm(options)),
|
|
285
|
+
setAlarm: (scheduledTime: number | Date, options?: globalThis.DurableObjectSetAlarmOptions) =>
|
|
286
|
+
tryStoragePromise("transaction.setAlarm", () => txn.setAlarm(scheduledTime, options)),
|
|
287
|
+
deleteAlarm: (options?: globalThis.DurableObjectSetAlarmOptions) =>
|
|
288
|
+
tryStoragePromise("transaction.deleteAlarm", () => txn.deleteAlarm(options)),
|
|
289
|
+
}) as DurableObjectTransaction;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Wraps native Cloudflare storage APIs as Effect-returning helpers.
|
|
293
|
+
*/
|
|
294
|
+
export const fromDurableObjectStorage = (
|
|
295
|
+
storage: globalThis.DurableObjectStorage,
|
|
296
|
+
): DurableObjectStorage => ({
|
|
297
|
+
get: <T = unknown>(key: string, options?: globalThis.DurableObjectGetOptions) =>
|
|
298
|
+
tryStoragePromise("get", () => storage.get<T>(key, options)),
|
|
299
|
+
put: <T>(key: string, value: T, options?: globalThis.DurableObjectPutOptions) =>
|
|
300
|
+
tryStoragePromise("put", () => storage.put(key, value, options)),
|
|
301
|
+
delete: (key: string, options?: globalThis.DurableObjectPutOptions) =>
|
|
302
|
+
tryStoragePromise("delete", () => storage.delete(key, options)),
|
|
303
|
+
deleteAll: (options?: globalThis.DurableObjectPutOptions) =>
|
|
304
|
+
tryStoragePromise("deleteAll", () => storage.deleteAll(options)),
|
|
305
|
+
getAlarm: (options?: globalThis.DurableObjectGetAlarmOptions) =>
|
|
306
|
+
tryStoragePromise("getAlarm", () => storage.getAlarm(options)),
|
|
307
|
+
setAlarm: (scheduledTime: number | Date, options?: globalThis.DurableObjectSetAlarmOptions) =>
|
|
308
|
+
tryStoragePromise("setAlarm", () => storage.setAlarm(scheduledTime, options)),
|
|
309
|
+
deleteAlarm: (options?: globalThis.DurableObjectSetAlarmOptions) =>
|
|
310
|
+
tryStoragePromise("deleteAlarm", () => storage.deleteAlarm(options)),
|
|
311
|
+
transactionSync: <A, E, R>(closure: () => Effect.Effect<A, E, R>) =>
|
|
312
|
+
Effect.context<R>().pipe(
|
|
313
|
+
Effect.flatMap((context) =>
|
|
314
|
+
Effect.suspend(() => {
|
|
315
|
+
try {
|
|
316
|
+
return Effect.succeed(
|
|
317
|
+
storage.transactionSync(() => {
|
|
318
|
+
const exit = Effect.runSyncExitWith(context)(closure());
|
|
319
|
+
|
|
320
|
+
if (Exit.isSuccess(exit)) {
|
|
321
|
+
return exit.value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw exit;
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
} catch (cause) {
|
|
328
|
+
if (Exit.isExit(cause) && Exit.isFailure(cause)) {
|
|
329
|
+
return Effect.failCause(cause.cause) as Effect.Effect<
|
|
330
|
+
A,
|
|
331
|
+
E | StorageOperationError,
|
|
332
|
+
R
|
|
333
|
+
>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return Effect.fail(storageError("transactionSync", cause));
|
|
337
|
+
}
|
|
338
|
+
}),
|
|
339
|
+
),
|
|
340
|
+
),
|
|
341
|
+
transaction: <A, E, R>(closure: (txn: DurableObjectTransaction) => Effect.Effect<A, E, R>) =>
|
|
342
|
+
Effect.context<R>().pipe(
|
|
343
|
+
Effect.flatMap((context) =>
|
|
344
|
+
Effect.callback<A, E | StorageOperationError>((resume) => {
|
|
345
|
+
void storage
|
|
346
|
+
.transaction(async (txn) => {
|
|
347
|
+
const exit = await Effect.runPromiseExitWith(context)(
|
|
348
|
+
closure(fromDurableObjectTransaction(txn)),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (Exit.isSuccess(exit)) {
|
|
352
|
+
return exit.value;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
throw exit;
|
|
356
|
+
})
|
|
357
|
+
.then(
|
|
358
|
+
(value) => resume(Effect.succeed(value)),
|
|
359
|
+
(cause) => {
|
|
360
|
+
if (Exit.isExit(cause) && Exit.isFailure(cause)) {
|
|
361
|
+
resume(
|
|
362
|
+
Effect.failCause(cause.cause) as Effect.Effect<A, E | StorageOperationError>,
|
|
363
|
+
);
|
|
364
|
+
} else {
|
|
365
|
+
resume(Effect.fail(storageError("transaction", cause)));
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
}),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
sync: () => tryStoragePromise("sync", () => storage.sync()),
|
|
373
|
+
getCurrentBookmark: () =>
|
|
374
|
+
tryStoragePromise("getCurrentBookmark", () => storage.getCurrentBookmark()),
|
|
375
|
+
onNextSessionRestoreBookmark: (bookmark: string) =>
|
|
376
|
+
tryStoragePromise("onNextSessionRestoreBookmark", () =>
|
|
377
|
+
storage.onNextSessionRestoreBookmark(bookmark),
|
|
378
|
+
),
|
|
379
|
+
sql: fromSqlStorage(storage.sql),
|
|
380
|
+
kv: fromSyncKvStorage(storage.kv),
|
|
381
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import { DurableObjectState } from "./DurableObjectState";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for accepting an incoming websocket request in a Durable Object.
|
|
7
|
+
*/
|
|
8
|
+
export interface AcceptUpgradeOptions<Attachment = unknown> {
|
|
9
|
+
/** Optional websocket tags for Durable Object hibernation filtering. */
|
|
10
|
+
readonly tags?: ReadonlyArray<string> | undefined;
|
|
11
|
+
/** Optional attachment serialized onto the server socket. */
|
|
12
|
+
readonly attachment?: Attachment | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of a websocket upgrade accepted by {@link acceptUpgrade}.
|
|
17
|
+
*/
|
|
18
|
+
export interface AcceptedUpgrade {
|
|
19
|
+
readonly client: WebSocket;
|
|
20
|
+
readonly server: WebSocket;
|
|
21
|
+
readonly response: Response;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Accepts a websocket upgrade and registers the server socket on `DurableObjectState`.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const response = yield* DurableObjectWebSocket.acceptUpgrade({
|
|
30
|
+
* tags: ["room:general"],
|
|
31
|
+
* });
|
|
32
|
+
*
|
|
33
|
+
* return response.response;
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const acceptUpgrade = <Attachment = unknown>(
|
|
37
|
+
options: AcceptUpgradeOptions<Attachment> = {},
|
|
38
|
+
): Effect.Effect<AcceptedUpgrade, never, DurableObjectState> =>
|
|
39
|
+
Effect.gen(function* () {
|
|
40
|
+
const state = yield* DurableObjectState;
|
|
41
|
+
const pair = new WebSocketPair();
|
|
42
|
+
const client = pair[0];
|
|
43
|
+
const server = pair[1];
|
|
44
|
+
|
|
45
|
+
if (options.attachment !== undefined) {
|
|
46
|
+
server.serializeAttachment(options.attachment);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
yield* state.acceptWebSocket(
|
|
50
|
+
server,
|
|
51
|
+
options.tags === undefined ? undefined : [...options.tags],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
client,
|
|
56
|
+
server,
|
|
57
|
+
response: new Response(null, {
|
|
58
|
+
status: 101,
|
|
59
|
+
webSocket: client,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
});
|