@uploadista/kv-store-cloudflare-do 0.0.3
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +616 -0
- package/dist/cloudflare-do-flow-store.d.ts +11 -0
- package/dist/cloudflare-do-flow-store.d.ts.map +1 -0
- package/dist/cloudflare-do-flow-store.js +40 -0
- package/dist/cloudflare-do-store.d.ts +7 -0
- package/dist/cloudflare-do-store.d.ts.map +1 -0
- package/dist/cloudflare-do-store.js +28 -0
- package/dist/cloudflare-do-upload-store.d.ts +9 -0
- package/dist/cloudflare-do-upload-store.d.ts.map +1 -0
- package/dist/cloudflare-do-upload-store.js +38 -0
- package/dist/flowjob-durable-object.d.ts +10 -0
- package/dist/flowjob-durable-object.d.ts.map +1 -0
- package/dist/flowjob-durable-object.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/uploadfile-durable-object.d.ts +10 -0
- package/dist/uploadfile-durable-object.d.ts.map +1 -0
- package/dist/uploadfile-durable-object.js +1 -0
- package/package.json +29 -0
- package/src/cloudflare-do-flow-store.ts +61 -0
- package/src/cloudflare-do-upload-store.ts +63 -0
- package/src/flowjob-durable-object.ts +15 -0
- package/src/index.ts +4 -0
- package/src/uploadfile-durable-object.ts +14 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 uploadista
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# @uploadista/kv-store-cloudflare-do
|
|
2
|
+
|
|
3
|
+
Cloudflare Durable Objects-backed key-value store for Uploadista. Provides globally consistent state with real-time coordination at the edge.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The Cloudflare Durable Objects store uses Durable Objects for storing upload and flow state with strong consistency guarantees. Perfect for:
|
|
8
|
+
|
|
9
|
+
- **Real-Time Consistency**: Strong consistency across all operations
|
|
10
|
+
- **WebSocket Coordination**: Persistent connections for real-time updates
|
|
11
|
+
- **Geo-Partitioned State**: Automatic data locality near users
|
|
12
|
+
- **Transactional Operations**: ACID guarantees for complex workflows
|
|
13
|
+
- **Edge Storage**: Data stored in the region closest to your users
|
|
14
|
+
|
|
15
|
+
More powerful than KV for upload scenarios requiring state coordination and real-time updates.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @uploadista/kv-store-cloudflare-do
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @uploadista/kv-store-cloudflare-do
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Prerequisites
|
|
26
|
+
|
|
27
|
+
- Cloudflare Workers project with Durable Objects enabled
|
|
28
|
+
- `@cloudflare/workers-types` for type definitions
|
|
29
|
+
- Durable Objects bindings configured in `wrangler.toml`
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { cloudflareDoFlowJobKvStore } from "@uploadista/kv-store-cloudflare-do";
|
|
35
|
+
import type { FlowJob } from "@uploadista/core";
|
|
36
|
+
import { Effect } from "effect";
|
|
37
|
+
|
|
38
|
+
export interface Env {
|
|
39
|
+
FLOW_JOB_STORE: DurableObjectNamespace;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
async fetch(request: Request, env: Env) {
|
|
44
|
+
const program = Effect.gen(function* () {
|
|
45
|
+
// The flow job store is available
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return Effect.runPromise(
|
|
49
|
+
program.pipe(
|
|
50
|
+
Effect.provide(
|
|
51
|
+
cloudflareDoFlowJobKvStore({
|
|
52
|
+
durableObject: env.FLOW_JOB_STORE,
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- ✅ **Strong Consistency**: ACID properties for state operations
|
|
64
|
+
- ✅ **Real-Time WebSockets**: Native support for persistent connections
|
|
65
|
+
- ✅ **Global Coordination**: Coordinate uploads across regions
|
|
66
|
+
- ✅ **Automatic Failover**: Built-in replication and redundancy
|
|
67
|
+
- ✅ **High Performance**: Single-digit millisecond latency
|
|
68
|
+
- ✅ **Stateful Workflows**: Maintain upload progress and flow execution state
|
|
69
|
+
- ✅ **Event Coordination**: Built-in for triggering flows after uploads
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### Main Exports
|
|
74
|
+
|
|
75
|
+
#### `cloudflareDoFlowJobKvStore(config: CloudflareDoFlowStoreOptions): Layer<FlowJobKVStore>`
|
|
76
|
+
|
|
77
|
+
Creates an Effect layer for storing flow jobs with Durable Objects.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { cloudflareDoFlowJobKvStore } from "@uploadista/kv-store-cloudflare-do";
|
|
81
|
+
|
|
82
|
+
const layer = cloudflareDoFlowJobKvStore({
|
|
83
|
+
durableObject: env.FLOW_JOB_STORE,
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Configuration**:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
type CloudflareDoFlowStoreOptions = {
|
|
91
|
+
durableObject: FlowJobDurableObject<FlowJob>;
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `makeCloudflareDoFlowStore<T extends FlowJob>(config: CloudflareDoFlowStoreOptions): KvStore<T>`
|
|
96
|
+
|
|
97
|
+
Factory function for creating a typed flow job store.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { makeCloudflareDoFlowStore } from "@uploadista/kv-store-cloudflare-do";
|
|
101
|
+
import type { FlowJob } from "@uploadista/core";
|
|
102
|
+
|
|
103
|
+
const store = makeCloudflareDoFlowStore<FlowJob>({
|
|
104
|
+
durableObject: env.FLOW_JOB_STORE,
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Available Operations
|
|
109
|
+
|
|
110
|
+
The Durable Objects store implements the `KvStore<T>` interface:
|
|
111
|
+
|
|
112
|
+
#### `get(key: string): Effect<T>`
|
|
113
|
+
|
|
114
|
+
Retrieve a flow job. Throws error if not found.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const program = Effect.gen(function* () {
|
|
118
|
+
const job = yield* store.get("flow:abc123");
|
|
119
|
+
// Strongly consistent read
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `set(key: string, value: T): Effect<void>`
|
|
124
|
+
|
|
125
|
+
Store a flow job with ACID guarantees.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const program = Effect.gen(function* () {
|
|
129
|
+
yield* store.set("flow:abc123", flowJob);
|
|
130
|
+
// Atomically persisted
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `delete(key: string): Effect<void>`
|
|
135
|
+
|
|
136
|
+
Remove a flow job.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const program = Effect.gen(function* () {
|
|
140
|
+
yield* store.delete("flow:abc123");
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
### Basic Setup in wrangler.toml
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
name = "uploadista-worker"
|
|
150
|
+
main = "src/index.ts"
|
|
151
|
+
|
|
152
|
+
[[durable_objects.bindings]]
|
|
153
|
+
name = "FLOW_JOB_STORE"
|
|
154
|
+
class_name = "FlowJobDurableObject"
|
|
155
|
+
|
|
156
|
+
[env.production]
|
|
157
|
+
durable_objects = { bindings = [{name = "FLOW_JOB_STORE", class_name = "FlowJobDurableObject"}] }
|
|
158
|
+
routes = [
|
|
159
|
+
{ pattern = "uploadista.example.com/*", zone_name = "example.com" }
|
|
160
|
+
]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Worker Environment Setup
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { cloudflareDoFlowJobKvStore } from "@uploadista/kv-store-cloudflare-do";
|
|
167
|
+
|
|
168
|
+
export interface Env {
|
|
169
|
+
FLOW_JOB_STORE: DurableObjectNamespace;
|
|
170
|
+
ENVIRONMENT: "production" | "staging";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default {
|
|
174
|
+
async fetch(request: Request, env: Env) {
|
|
175
|
+
const program = Effect.gen(function* () {
|
|
176
|
+
// Use flow job store
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return Effect.runPromise(
|
|
180
|
+
program.pipe(
|
|
181
|
+
Effect.provide(
|
|
182
|
+
cloudflareDoFlowJobKvStore({
|
|
183
|
+
durableObject: env.FLOW_JOB_STORE,
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Examples
|
|
193
|
+
|
|
194
|
+
### Example 1: Tracking Long-Running Flow Jobs
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { cloudflareDoFlowJobKvStore } from "@uploadista/kv-store-cloudflare-do";
|
|
198
|
+
import type { FlowJob } from "@uploadista/core";
|
|
199
|
+
import { Effect } from "effect";
|
|
200
|
+
|
|
201
|
+
export interface Env {
|
|
202
|
+
FLOW_JOB_STORE: DurableObjectNamespace;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const trackFlowJob = (
|
|
206
|
+
store: KvStore<FlowJob>,
|
|
207
|
+
jobId: string,
|
|
208
|
+
job: FlowJob
|
|
209
|
+
) =>
|
|
210
|
+
Effect.gen(function* () {
|
|
211
|
+
// Store initial job state
|
|
212
|
+
yield* store.set(jobId, {
|
|
213
|
+
...job,
|
|
214
|
+
status: "running",
|
|
215
|
+
startedAt: new Date().toISOString(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Simulate processing
|
|
219
|
+
yield* Effect.sleep("5 seconds");
|
|
220
|
+
|
|
221
|
+
// Update with completion
|
|
222
|
+
yield* store.set(jobId, {
|
|
223
|
+
...job,
|
|
224
|
+
status: "completed",
|
|
225
|
+
completedAt: new Date().toISOString(),
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
export default {
|
|
230
|
+
async fetch(request: Request, env: Env) {
|
|
231
|
+
const program = Effect.gen(function* () {
|
|
232
|
+
const store = yield* cloudflareDoFlowJobKvStore({
|
|
233
|
+
durableObject: env.FLOW_JOB_STORE,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
yield* trackFlowJob(store, "job:abc123", flowJob);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return Effect.runPromise(program);
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Example 2: WebSocket Progress Updates
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { cloudflareDoFlowJobKvStore } from "@uploadista/kv-store-cloudflare-do";
|
|
248
|
+
import type { FlowJob } from "@uploadista/core";
|
|
249
|
+
|
|
250
|
+
export default {
|
|
251
|
+
async fetch(request: Request, env: Env) {
|
|
252
|
+
// Handle WebSocket for real-time progress
|
|
253
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
254
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
255
|
+
|
|
256
|
+
// Store WebSocket connection
|
|
257
|
+
const jobId = "job:abc123";
|
|
258
|
+
const stub = env.FLOW_JOB_STORE.get(jobId);
|
|
259
|
+
|
|
260
|
+
// Register connection with Durable Object
|
|
261
|
+
await stub.registerWebSocket(server);
|
|
262
|
+
|
|
263
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Example 3: Coordinating Multi-Step Uploads
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { cloudflareDoFlowJobKvStore } from "@uploadista/kv-store-cloudflare-do";
|
|
273
|
+
import type { FlowJob } from "@uploadista/core";
|
|
274
|
+
import { Effect } from "effect";
|
|
275
|
+
|
|
276
|
+
interface UploadState {
|
|
277
|
+
uploadId: string;
|
|
278
|
+
chunks: Map<number, boolean>;
|
|
279
|
+
totalChunks: number;
|
|
280
|
+
metadata: Record<string, string>;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const coordinateUpload = (store: KvStore<UploadState>, uploadId: string) =>
|
|
284
|
+
Effect.gen(function* () {
|
|
285
|
+
// Get current state
|
|
286
|
+
const state = yield* store.get(uploadId);
|
|
287
|
+
|
|
288
|
+
// Check if all chunks received
|
|
289
|
+
const allChunksReceived = Array.from(state.chunks.values()).every(
|
|
290
|
+
(v) => v
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (allChunksReceived) {
|
|
294
|
+
// Trigger assembly
|
|
295
|
+
yield* store.set(uploadId, {
|
|
296
|
+
...state,
|
|
297
|
+
status: "assembling",
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Notify all connected clients via WebSocket
|
|
301
|
+
// Real-time coordination across regions
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Performance Characteristics
|
|
307
|
+
|
|
308
|
+
| Operation | Latency | Consistency |
|
|
309
|
+
|-----------|---------|-------------|
|
|
310
|
+
| get() | ~5ms | Strong |
|
|
311
|
+
| set() | ~10ms | ACID |
|
|
312
|
+
| delete() | ~10ms | ACID |
|
|
313
|
+
| Multi-step | ~50ms | Transactional |
|
|
314
|
+
|
|
315
|
+
All operations are strongly consistent - no eventual consistency delay.
|
|
316
|
+
|
|
317
|
+
## Limits & Quotas
|
|
318
|
+
|
|
319
|
+
| Limit | Value |
|
|
320
|
+
|-------|-------|
|
|
321
|
+
| Storage per Object | 128 MB |
|
|
322
|
+
| Request Rate | 1,000 r/s per object |
|
|
323
|
+
| Simultaneous WebSockets | 128 per object |
|
|
324
|
+
| Transactional Groups | 128 per transaction |
|
|
325
|
+
|
|
326
|
+
For most upload use cases, these are more than sufficient.
|
|
327
|
+
|
|
328
|
+
## Use Cases
|
|
329
|
+
|
|
330
|
+
### Perfect For
|
|
331
|
+
|
|
332
|
+
- ✅ Real-time upload tracking and progress
|
|
333
|
+
- ✅ Complex multi-step workflows with coordination
|
|
334
|
+
- ✅ WebSocket connections for client progress updates
|
|
335
|
+
- ✅ Flow jobs requiring transactional guarantees
|
|
336
|
+
- ✅ Partitioned state near users globally
|
|
337
|
+
|
|
338
|
+
### Better Alternatives
|
|
339
|
+
|
|
340
|
+
- ❌ Simple key-value storage → Use KV instead
|
|
341
|
+
- ❌ Large file uploads (>128MB per object) → Use KV + R2
|
|
342
|
+
- ❌ Extremely high write rates (>1000/sec) → Use database
|
|
343
|
+
|
|
344
|
+
## Comparison with KV
|
|
345
|
+
|
|
346
|
+
| Feature | Durable Objects | KV |
|
|
347
|
+
|---------|-----------------|-----|
|
|
348
|
+
| Consistency | Strong | Eventual (~30s) |
|
|
349
|
+
| WebSockets | Built-in | Requires separate service |
|
|
350
|
+
| Storage | 128 MB/object | Unlimited |
|
|
351
|
+
| Transaction Support | Full ACID | Individual ops only |
|
|
352
|
+
| Coordination | Excellent | Limited |
|
|
353
|
+
| Cost | Higher | Lower |
|
|
354
|
+
| Complexity | Higher | Lower |
|
|
355
|
+
|
|
356
|
+
## Scaling Patterns
|
|
357
|
+
|
|
358
|
+
### Single Durable Object
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
All Clients ──→ Single Durable Object ──→ State File
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Suitable for modest traffic (1,000s of jobs).
|
|
365
|
+
|
|
366
|
+
### Partitioned by Upload ID
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
Client A ──→ Object 1 (uploads 0-999) ──→ Storage
|
|
370
|
+
Client B ──→ Object 2 (uploads 1000-1999) ──→ Storage
|
|
371
|
+
Client C ──→ Object 3 (uploads 2000+) ──→ Storage
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Automatic partitioning via `idFromName()`:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const stub = env.FLOW_JOB_STORE.idFromName(uploadId);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Hierarchical Objects
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
Upload Object (coordination) ┐
|
|
384
|
+
├─ Chunk Object 1 ├─ Flow Job Objects
|
|
385
|
+
├─ Chunk Object 2 │
|
|
386
|
+
└─ Chunk Object 3 │
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Best Practices
|
|
390
|
+
|
|
391
|
+
### 1. Use Deterministic Naming
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// Good: Same input always gets same object
|
|
395
|
+
const id = durableObject.idFromName(`upload:${uploadId}`);
|
|
396
|
+
|
|
397
|
+
// Avoid: Random IDs (creates new objects every time)
|
|
398
|
+
const id = durableObject.newUniqueId();
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### 2. Handle Durable Object Resets
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const getOrCreateJob = (store: KvStore<FlowJob>, jobId: string) =>
|
|
405
|
+
Effect.gen(function* () {
|
|
406
|
+
try {
|
|
407
|
+
return yield* store.get(jobId);
|
|
408
|
+
} catch (e) {
|
|
409
|
+
// Object may have reset, recreate
|
|
410
|
+
const newJob = createDefaultJob(jobId);
|
|
411
|
+
yield* store.set(jobId, newJob);
|
|
412
|
+
return newJob;
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### 3. Clean Up Old Objects
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// Set cleanup schedule via middleware
|
|
421
|
+
export default {
|
|
422
|
+
async fetch(request: Request, env: Env) {
|
|
423
|
+
if (request.url.includes("/cleanup")) {
|
|
424
|
+
// Delete old Durable Objects
|
|
425
|
+
// Implement retention policy
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### 4. Monitor Storage Usage
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const checkObjectSize = (stub: DurableObjectStub) =>
|
|
435
|
+
Effect.gen(function* () {
|
|
436
|
+
const info = yield* Effect.tryPromise({
|
|
437
|
+
try: () => stub.getMetadata(),
|
|
438
|
+
catch: () => null,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (info && info.storageDelta > 100 * 1024 * 1024) {
|
|
442
|
+
// Object exceeds 100MB, consider partitioning
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Deployment
|
|
448
|
+
|
|
449
|
+
### Local Development
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
# Enable Durable Objects in wrangler
|
|
453
|
+
wrangler dev
|
|
454
|
+
|
|
455
|
+
# Test locally with http://localhost:8787
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Production Deployment
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Deploy with Durable Objects
|
|
462
|
+
wrangler publish
|
|
463
|
+
|
|
464
|
+
# Migrate data between environments
|
|
465
|
+
wrangler migrations create move_objects
|
|
466
|
+
|
|
467
|
+
# Back up Durable Objects
|
|
468
|
+
wrangler durable-objects backup
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### GitHub Actions
|
|
472
|
+
|
|
473
|
+
```yaml
|
|
474
|
+
name: Deploy
|
|
475
|
+
|
|
476
|
+
on:
|
|
477
|
+
push:
|
|
478
|
+
branches: [main]
|
|
479
|
+
|
|
480
|
+
jobs:
|
|
481
|
+
deploy:
|
|
482
|
+
runs-on: ubuntu-latest
|
|
483
|
+
steps:
|
|
484
|
+
- uses: actions/checkout@v3
|
|
485
|
+
- uses: cloudflare/wrangler-action@v3
|
|
486
|
+
with:
|
|
487
|
+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
488
|
+
secrets: |
|
|
489
|
+
DURABLE_OBJECT_ENCRYPTION_KEY
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Integration with Other Services
|
|
493
|
+
|
|
494
|
+
### Cloudflare KV for Hot Cache
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
export interface Env {
|
|
498
|
+
FLOW_JOB_STORE: DurableObjectNamespace;
|
|
499
|
+
KV_CACHE: KVNamespace;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Use DO for coordination, KV for fast reads
|
|
503
|
+
const getJobWithCache = (env: Env, jobId: string) =>
|
|
504
|
+
Effect.gen(function* () {
|
|
505
|
+
// Try KV first
|
|
506
|
+
const cached = yield* Effect.tryPromise({
|
|
507
|
+
try: () => env.KV_CACHE.get(`job:${jobId}`),
|
|
508
|
+
catch: () => null,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
if (cached) {
|
|
512
|
+
return JSON.parse(cached);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Fall back to DO
|
|
516
|
+
const stub = env.FLOW_JOB_STORE.get(env.FLOW_JOB_STORE.idFromName(jobId));
|
|
517
|
+
const job = yield* Effect.tryPromise({
|
|
518
|
+
try: () => stub.getFlowJob(),
|
|
519
|
+
catch: () => null,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Cache in KV
|
|
523
|
+
if (job) {
|
|
524
|
+
yield* Effect.tryPromise({
|
|
525
|
+
try: () =>
|
|
526
|
+
env.KV_CACHE.put(`job:${jobId}`, JSON.stringify(job), {
|
|
527
|
+
expirationTtl: 60,
|
|
528
|
+
}),
|
|
529
|
+
catch: () => null,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return job;
|
|
534
|
+
});
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Cloudflare D1 for Persistent Store
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// Use DO as cache layer, D1 as durable storage
|
|
541
|
+
const persistJob = (env: Env, job: FlowJob) =>
|
|
542
|
+
Effect.gen(function* () {
|
|
543
|
+
// Write to DO immediately
|
|
544
|
+
yield* store.set(job.id, job);
|
|
545
|
+
|
|
546
|
+
// Persist to D1 asynchronously
|
|
547
|
+
await env.DB.prepare(
|
|
548
|
+
"INSERT INTO jobs (id, data) VALUES (?, ?)"
|
|
549
|
+
).bind(job.id, JSON.stringify(job)).run();
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
## Related Packages
|
|
554
|
+
|
|
555
|
+
- [@uploadista/core](../../core) - Core types
|
|
556
|
+
- [@uploadista/kv-store-cloudflare-kv](../cloudflare-kv) - For simpler use cases
|
|
557
|
+
- [@uploadista/adapters-hono](../../servers/adapters-hono) - Hono integration
|
|
558
|
+
- [@uploadista/event-emitter-durable-object](../../event-emitters/event-emitter-durable-object) - Real-time events
|
|
559
|
+
- [@uploadista/server](../../servers/server) - Upload server
|
|
560
|
+
|
|
561
|
+
## Troubleshooting
|
|
562
|
+
|
|
563
|
+
### "Durable Object not found" Error
|
|
564
|
+
|
|
565
|
+
Ensure binding is defined in `wrangler.toml`:
|
|
566
|
+
|
|
567
|
+
```toml
|
|
568
|
+
[[durable_objects.bindings]]
|
|
569
|
+
name = "FLOW_JOB_STORE"
|
|
570
|
+
class_name = "FlowJobDurableObject"
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Then re-run `wrangler dev`.
|
|
574
|
+
|
|
575
|
+
### Storage Exceeds 128MB
|
|
576
|
+
|
|
577
|
+
If Durable Object reaches storage limit:
|
|
578
|
+
|
|
579
|
+
1. Implement partitioning (multiple objects)
|
|
580
|
+
2. Archive old jobs to D1/R2
|
|
581
|
+
3. Clean up completed jobs regularly
|
|
582
|
+
|
|
583
|
+
### WebSocket Disconnections
|
|
584
|
+
|
|
585
|
+
For stable WebSocket connections:
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
server.addEventListener("message", async (event) => {
|
|
589
|
+
try {
|
|
590
|
+
await processMessage(event.data);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
// Reconnect client
|
|
593
|
+
server.close(1011, "Server error");
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### High Request Latency
|
|
599
|
+
|
|
600
|
+
If requests are slow:
|
|
601
|
+
|
|
602
|
+
1. Check object partition count (may need more)
|
|
603
|
+
2. Reduce data size per operation
|
|
604
|
+
3. Implement batching on client
|
|
605
|
+
|
|
606
|
+
## License
|
|
607
|
+
|
|
608
|
+
See [LICENSE](../../../LICENSE) in the main repository.
|
|
609
|
+
|
|
610
|
+
## See Also
|
|
611
|
+
|
|
612
|
+
- [KV Stores Comparison Guide](../KV_STORES_COMPARISON.md) - Compare with other options
|
|
613
|
+
- [Cloudflare Durable Objects Docs](https://developers.cloudflare.com/workers/runtime-apis/durable-objects/) - Official docs
|
|
614
|
+
- [Server Setup Guide](../../../SERVER_SETUP.md) - Deployment guide
|
|
615
|
+
- [Event Emitter with Durable Objects](../../event-emitters/event-emitter-durable-object/README.md) - Real-time events
|
|
616
|
+
- [@uploadista/adapters-hono](../../servers/adapters-hono) - Hono integration for Workers
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FlowJob } from "@uploadista/core/flow";
|
|
2
|
+
import { type KvStore, FlowJobKVStore } from "@uploadista/core/types";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
import type { FlowJobDurableObject } from "./flowjob-durable-object";
|
|
5
|
+
export type CloudflareDoFlowStoreOptions = {
|
|
6
|
+
durableObject: FlowJobDurableObject<FlowJob>;
|
|
7
|
+
};
|
|
8
|
+
export declare function makeCloudflareDoFlowStore<T extends FlowJob>({ durableObject, }: CloudflareDoFlowStoreOptions): KvStore<T>;
|
|
9
|
+
export declare const cloudflareDoFlowStore: typeof makeCloudflareDoFlowStore;
|
|
10
|
+
export declare const cloudflareDoFlowJobKvStore: (config: CloudflareDoFlowStoreOptions) => Layer.Layer<FlowJobKVStore, never, never>;
|
|
11
|
+
//# sourceMappingURL=cloudflare-do-flow-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-do-flow-store.d.ts","sourceRoot":"","sources":["../src/cloudflare-do-flow-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,KAAK,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,oBAAoB,EAErB,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,4BAA4B,GAAG;IACzC,aAAa,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC;CAC9C,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,OAAO,EAAE,EAC3D,aAAa,GACd,EAAE,4BAA4B,GAAG,OAAO,CAAC,CAAC,CAAC,CAsC3C;AAGD,eAAO,MAAM,qBAAqB,kCAA4B,CAAC;AAE/D,eAAO,MAAM,0BAA0B,GACrC,QAAQ,4BAA4B,8CAC+B,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { FlowJobKVStore } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
export function makeCloudflareDoFlowStore({ durableObject, }) {
|
|
5
|
+
function getStub(key) {
|
|
6
|
+
const id = durableObject.idFromName(key);
|
|
7
|
+
return durableObject.get(id);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
get: (key) => {
|
|
11
|
+
const stub = getStub(key);
|
|
12
|
+
return Effect.tryPromise({
|
|
13
|
+
try: () => stub.getFlowJob(),
|
|
14
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
15
|
+
}).pipe(Effect.flatMap((value) => {
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
return Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND"));
|
|
18
|
+
}
|
|
19
|
+
return Effect.succeed(value);
|
|
20
|
+
}));
|
|
21
|
+
},
|
|
22
|
+
set: (key, value) => {
|
|
23
|
+
const stub = getStub(key);
|
|
24
|
+
return Effect.tryPromise({
|
|
25
|
+
try: () => stub.setFlowJob(value),
|
|
26
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
27
|
+
}).pipe(Effect.asVoid);
|
|
28
|
+
},
|
|
29
|
+
delete: (key) => {
|
|
30
|
+
const stub = getStub(key);
|
|
31
|
+
return Effect.tryPromise({
|
|
32
|
+
try: () => stub.deleteFlowJob(),
|
|
33
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
34
|
+
}).pipe(Effect.asVoid);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Legacy function for backward compatibility
|
|
39
|
+
export const cloudflareDoFlowStore = makeCloudflareDoFlowStore;
|
|
40
|
+
export const cloudflareDoFlowJobKvStore = (config) => Layer.succeed(FlowJobKVStore, makeCloudflareDoFlowStore(config));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { KvStore } from "@uploadista/core/types/kv-store";
|
|
2
|
+
import type { UploadFile } from "@uploadista/core/types/upload-file";
|
|
3
|
+
import type { UploadFileDurableObject } from "./uploadfile-durable-object";
|
|
4
|
+
export declare function cloudflareDoStore<T extends UploadFile>({ durableObject, }: {
|
|
5
|
+
durableObject: UploadFileDurableObject<T>;
|
|
6
|
+
}): KvStore<T>;
|
|
7
|
+
//# sourceMappingURL=cloudflare-do-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-do-store.d.ts","sourceRoot":"","sources":["../src/cloudflare-do-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAE/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,KAAK,EACV,uBAAuB,EAExB,MAAM,6BAA6B,CAAC;AAErC,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EAAE,EACtD,aAAa,GACd,EAAE;IACD,aAAa,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,CAAC,CAAC,CA8Bb"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function cloudflareDoStore({ durableObject, }) {
|
|
2
|
+
function getStub(key) {
|
|
3
|
+
const id = durableObject.idFromName(key);
|
|
4
|
+
return durableObject.get(id);
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
get: async (key) => {
|
|
8
|
+
const stub = getStub(key);
|
|
9
|
+
const result = await stub.getUploadFile();
|
|
10
|
+
return result;
|
|
11
|
+
},
|
|
12
|
+
set: async (key, value) => {
|
|
13
|
+
const stub = getStub(key);
|
|
14
|
+
await stub.setUploadFile(value);
|
|
15
|
+
},
|
|
16
|
+
delete: async (key) => {
|
|
17
|
+
const stub = getStub(key);
|
|
18
|
+
await stub.deleteUploadFile();
|
|
19
|
+
},
|
|
20
|
+
emit: async (key, event) => {
|
|
21
|
+
const stub = getStub(key);
|
|
22
|
+
const result = await stub.getUploadFile();
|
|
23
|
+
if (result) {
|
|
24
|
+
await stub.emit(event);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type KvStore, type UploadFile, UploadFileKVStore } from "@uploadista/core/types";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import type { UploadFileDurableObject } from "./uploadfile-durable-object";
|
|
4
|
+
export type CloudflareDoUploadStoreOptions = {
|
|
5
|
+
durableObject: UploadFileDurableObject<UploadFile>;
|
|
6
|
+
};
|
|
7
|
+
export declare function makeCloudflareDoUploadStore<T extends UploadFile>({ durableObject, }: CloudflareDoUploadStoreOptions): KvStore<T>;
|
|
8
|
+
export declare const cloudflareDoUploadStore: (config: CloudflareDoUploadStoreOptions) => Layer.Layer<UploadFileKVStore, never, never>;
|
|
9
|
+
//# sourceMappingURL=cloudflare-do-upload-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-do-upload-store.d.ts","sourceRoot":"","sources":["../src/cloudflare-do-upload-store.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,UAAU,EACf,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EACV,uBAAuB,EAExB,MAAM,6BAA6B,CAAC;AAErC,MAAM,MAAM,8BAA8B,GAAG;IAC3C,aAAa,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAC;CACpD,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,UAAU,EAAE,EAChE,aAAa,GACd,EAAE,8BAA8B,GAAG,OAAO,CAAC,CAAC,CAAC,CAwC7C;AAED,eAAO,MAAM,uBAAuB,GAClC,QAAQ,8BAA8B,iDACkC,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { UploadFileKVStore, } from "@uploadista/core/types";
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
export function makeCloudflareDoUploadStore({ durableObject, }) {
|
|
5
|
+
function getStub(key) {
|
|
6
|
+
const id = durableObject.idFromName(key);
|
|
7
|
+
return durableObject.get(id);
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
get: (key) => {
|
|
11
|
+
const stub = getStub(key);
|
|
12
|
+
return Effect.tryPromise({
|
|
13
|
+
try: () => stub.getUploadFile(),
|
|
14
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
15
|
+
}).pipe(Effect.flatMap((value) => {
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
return Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND"));
|
|
18
|
+
}
|
|
19
|
+
return Effect.succeed(value);
|
|
20
|
+
}));
|
|
21
|
+
},
|
|
22
|
+
set: (key, value) => {
|
|
23
|
+
const stub = getStub(key);
|
|
24
|
+
return Effect.tryPromise({
|
|
25
|
+
try: () => stub.setUploadFile(value),
|
|
26
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
27
|
+
}).pipe(Effect.asVoid);
|
|
28
|
+
},
|
|
29
|
+
delete: (key) => {
|
|
30
|
+
const stub = getStub(key);
|
|
31
|
+
return Effect.tryPromise({
|
|
32
|
+
try: () => stub.deleteUploadFile(),
|
|
33
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
34
|
+
}).pipe(Effect.asVoid);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const cloudflareDoUploadStore = (config) => Layer.succeed(UploadFileKVStore, makeCloudflareDoUploadStore(config));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
|
+
import type { FlowEvent, FlowJob } from "@uploadista/core/flow";
|
|
3
|
+
export type FlowJobDurableObjectBranded<T extends FlowJob> = Rpc.DurableObjectBranded & {
|
|
4
|
+
getFlowJob: () => Promise<T | undefined>;
|
|
5
|
+
setFlowJob: (value: T) => Promise<void>;
|
|
6
|
+
deleteFlowJob: () => Promise<void>;
|
|
7
|
+
emit: (event: FlowEvent) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type FlowJobDurableObject<T extends FlowJob> = DurableObjectNamespace<FlowJobDurableObjectBranded<T>>;
|
|
10
|
+
//# sourceMappingURL=flowjob-durable-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flowjob-durable-object.d.ts","sourceRoot":"","sources":["../src/flowjob-durable-object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEhE,MAAM,MAAM,2BAA2B,CAAC,CAAC,SAAS,OAAO,IACvD,GAAG,CAAC,oBAAoB,GAAG;IACzB,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzC,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C,CAAC;AAGJ,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,OAAO,IAAI,sBAAsB,CAC1E,2BAA2B,CAAC,CAAC,CAAC,CAC/B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
|
+
import type { UploadEventType, UploadFile } from "@uploadista/core/types";
|
|
3
|
+
export type UploadFileDurableObjectBranded<T extends UploadFile> = Rpc.DurableObjectBranded & {
|
|
4
|
+
getUploadFile: () => Promise<T | undefined>;
|
|
5
|
+
setUploadFile: (value: T) => Promise<void>;
|
|
6
|
+
deleteUploadFile: () => Promise<void>;
|
|
7
|
+
emit: (event: UploadEventType) => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
export type UploadFileDurableObject<T extends UploadFile> = DurableObjectNamespace<UploadFileDurableObjectBranded<T>>;
|
|
10
|
+
//# sourceMappingURL=uploadfile-durable-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploadfile-durable-object.d.ts","sourceRoot":"","sources":["../src/uploadfile-durable-object.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAE1E,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,UAAU,IAC7D,GAAG,CAAC,oBAAoB,GAAG;IACzB,aAAa,EAAE,MAAM,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC5C,aAAa,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD,CAAC;AAGJ,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,UAAU,IACtD,sBAAsB,CAAC,8BAA8B,CAAC,CAAC,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/kv-store-cloudflare-do",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.3",
|
|
5
|
+
"description": "Cloudflare Durable Object KV store for Uploadista",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Uploadista",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@cloudflare/workers-types": "4.20251011.0",
|
|
17
|
+
"effect": "3.18.4",
|
|
18
|
+
"@uploadista/core": "0.0.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@uploadista/typescript-config": "0.0.3"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b",
|
|
25
|
+
"format": "biome format --write ./src",
|
|
26
|
+
"lint": "biome lint --write ./src",
|
|
27
|
+
"check": "biome check --write ./src"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import type { FlowJob } from "@uploadista/core/flow";
|
|
3
|
+
import { type KvStore, FlowJobKVStore } from "@uploadista/core/types";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import type {
|
|
6
|
+
FlowJobDurableObject,
|
|
7
|
+
FlowJobDurableObjectBranded,
|
|
8
|
+
} from "./flowjob-durable-object";
|
|
9
|
+
|
|
10
|
+
export type CloudflareDoFlowStoreOptions = {
|
|
11
|
+
durableObject: FlowJobDurableObject<FlowJob>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function makeCloudflareDoFlowStore<T extends FlowJob>({
|
|
15
|
+
durableObject,
|
|
16
|
+
}: CloudflareDoFlowStoreOptions): KvStore<T> {
|
|
17
|
+
function getStub(key: string): FlowJobDurableObjectBranded<T> {
|
|
18
|
+
const id = durableObject.idFromName(key);
|
|
19
|
+
return durableObject.get(id) as unknown as FlowJobDurableObjectBranded<T>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
get: (key: string) => {
|
|
24
|
+
const stub = getStub(key);
|
|
25
|
+
return Effect.tryPromise({
|
|
26
|
+
try: () => stub.getFlowJob(),
|
|
27
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
28
|
+
}).pipe(
|
|
29
|
+
Effect.flatMap((value) => {
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND"));
|
|
32
|
+
}
|
|
33
|
+
return Effect.succeed(value);
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
set: (key: string, value: T) => {
|
|
39
|
+
const stub = getStub(key);
|
|
40
|
+
return Effect.tryPromise({
|
|
41
|
+
try: () => stub.setFlowJob(value),
|
|
42
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
43
|
+
}).pipe(Effect.asVoid);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
delete: (key: string) => {
|
|
47
|
+
const stub = getStub(key);
|
|
48
|
+
return Effect.tryPromise({
|
|
49
|
+
try: () => stub.deleteFlowJob(),
|
|
50
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
51
|
+
}).pipe(Effect.asVoid);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Legacy function for backward compatibility
|
|
57
|
+
export const cloudflareDoFlowStore = makeCloudflareDoFlowStore;
|
|
58
|
+
|
|
59
|
+
export const cloudflareDoFlowJobKvStore = (
|
|
60
|
+
config: CloudflareDoFlowStoreOptions,
|
|
61
|
+
) => Layer.succeed(FlowJobKVStore, makeCloudflareDoFlowStore(config));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import {
|
|
3
|
+
type KvStore,
|
|
4
|
+
type UploadFile,
|
|
5
|
+
UploadFileKVStore,
|
|
6
|
+
} from "@uploadista/core/types";
|
|
7
|
+
import { Effect, Layer } from "effect";
|
|
8
|
+
import type {
|
|
9
|
+
UploadFileDurableObject,
|
|
10
|
+
UploadFileDurableObjectBranded,
|
|
11
|
+
} from "./uploadfile-durable-object";
|
|
12
|
+
|
|
13
|
+
export type CloudflareDoUploadStoreOptions = {
|
|
14
|
+
durableObject: UploadFileDurableObject<UploadFile>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function makeCloudflareDoUploadStore<T extends UploadFile>({
|
|
18
|
+
durableObject,
|
|
19
|
+
}: CloudflareDoUploadStoreOptions): KvStore<T> {
|
|
20
|
+
function getStub(key: string): UploadFileDurableObjectBranded<T> {
|
|
21
|
+
const id = durableObject.idFromName(key);
|
|
22
|
+
return durableObject.get(
|
|
23
|
+
id,
|
|
24
|
+
) as unknown as UploadFileDurableObjectBranded<T>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
get: (key: string) => {
|
|
29
|
+
const stub = getStub(key);
|
|
30
|
+
return Effect.tryPromise({
|
|
31
|
+
try: () => stub.getUploadFile(),
|
|
32
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
33
|
+
}).pipe(
|
|
34
|
+
Effect.flatMap((value) => {
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
return Effect.fail(UploadistaError.fromCode("FILE_NOT_FOUND"));
|
|
37
|
+
}
|
|
38
|
+
return Effect.succeed(value);
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
set: (key: string, value: T) => {
|
|
44
|
+
const stub = getStub(key);
|
|
45
|
+
return Effect.tryPromise({
|
|
46
|
+
try: () => stub.setUploadFile(value),
|
|
47
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
48
|
+
}).pipe(Effect.asVoid);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
delete: (key: string) => {
|
|
52
|
+
const stub = getStub(key);
|
|
53
|
+
return Effect.tryPromise({
|
|
54
|
+
try: () => stub.deleteUploadFile(),
|
|
55
|
+
catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
|
|
56
|
+
}).pipe(Effect.asVoid);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const cloudflareDoUploadStore = (
|
|
62
|
+
config: CloudflareDoUploadStoreOptions,
|
|
63
|
+
) => Layer.succeed(UploadFileKVStore, makeCloudflareDoUploadStore(config));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
|
+
import type { FlowEvent, FlowJob } from "@uploadista/core/flow";
|
|
3
|
+
|
|
4
|
+
export type FlowJobDurableObjectBranded<T extends FlowJob> =
|
|
5
|
+
Rpc.DurableObjectBranded & {
|
|
6
|
+
getFlowJob: () => Promise<T | undefined>;
|
|
7
|
+
setFlowJob: (value: T) => Promise<void>;
|
|
8
|
+
deleteFlowJob: () => Promise<void>;
|
|
9
|
+
emit: (event: FlowEvent) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Durable Object
|
|
13
|
+
export type FlowJobDurableObject<T extends FlowJob> = DurableObjectNamespace<
|
|
14
|
+
FlowJobDurableObjectBranded<T>
|
|
15
|
+
>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DurableObjectNamespace, Rpc } from "@cloudflare/workers-types";
|
|
2
|
+
import type { UploadEventType, UploadFile } from "@uploadista/core/types";
|
|
3
|
+
|
|
4
|
+
export type UploadFileDurableObjectBranded<T extends UploadFile> =
|
|
5
|
+
Rpc.DurableObjectBranded & {
|
|
6
|
+
getUploadFile: () => Promise<T | undefined>;
|
|
7
|
+
setUploadFile: (value: T) => Promise<void>;
|
|
8
|
+
deleteUploadFile: () => Promise<void>;
|
|
9
|
+
emit: (event: UploadEventType) => Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Durable Object
|
|
13
|
+
export type UploadFileDurableObject<T extends UploadFile> =
|
|
14
|
+
DurableObjectNamespace<UploadFileDurableObjectBranded<T>>;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@uploadista/typescript-config/server.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": "./",
|
|
5
|
+
"paths": {
|
|
6
|
+
"@/*": ["./src/*"]
|
|
7
|
+
},
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"typeRoots": ["../../../../node_modules/@types"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src"]
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/cloudflare-do-flow-store.ts","./src/cloudflare-do-upload-store.ts","./src/flowjob-durable-object.ts","./src/index.ts","./src/uploadfile-durable-object.ts"],"version":"5.9.3"}
|