@uploadista/kv-store-cloudflare-do 0.0.13-beta.4 → 0.0.13

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@uploadista/kv-store-cloudflare-do",
3
3
  "type": "module",
4
- "version": "0.0.13-beta.4",
4
+ "version": "0.0.13",
5
5
  "description": "Cloudflare Durable Object KV store for Uploadista",
6
6
  "license": "MIT",
7
7
  "author": "Uploadista",
@@ -14,18 +14,23 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@cloudflare/workers-types": "4.20251106.1",
18
- "effect": "3.19.2",
19
- "@uploadista/core": "0.0.13-beta.4"
17
+ "@cloudflare/workers-types": "4.20251111.0",
18
+ "effect": "3.19.3",
19
+ "@uploadista/core": "0.0.13"
20
20
  },
21
21
  "devDependencies": {
22
- "tsdown": "0.16.0",
23
- "@uploadista/typescript-config": "0.0.13-beta.4"
22
+ "@effect/vitest": "0.27.0",
23
+ "tsdown": "0.16.3",
24
+ "vitest": "4.0.8",
25
+ "@uploadista/typescript-config": "0.0.13"
24
26
  },
25
27
  "scripts": {
26
28
  "build": "tsdown",
29
+ "check": "biome check --write ./src",
27
30
  "format": "biome format --write ./src",
28
31
  "lint": "biome lint --write ./src",
29
- "check": "biome check --write ./src"
32
+ "test": "vitest",
33
+ "test:run": "vitest run",
34
+ "test:watch": "vitest --watch"
30
35
  }
31
36
  }
@@ -0,0 +1,629 @@
1
+ import type { FlowJob } from "@uploadista/core/flow";
2
+ import type { UploadFile } from "@uploadista/core/types";
3
+ import { Effect } from "effect";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { makeCloudflareDoFlowStore } from "../src/cloudflare-do-flow-store";
6
+ import { makeCloudflareDoUploadStore } from "../src/cloudflare-do-upload-store";
7
+ import type {
8
+ FlowJobDurableObject,
9
+ FlowJobDurableObjectBranded,
10
+ } from "../src/flowjob-durable-object";
11
+ import type {
12
+ UploadFileDurableObject,
13
+ UploadFileDurableObjectBranded,
14
+ } from "../src/uploadfile-durable-object";
15
+
16
+ describe("Cloudflare Durable Objects KV Store", () => {
17
+ describe("FlowJob Store - Basic Operations", () => {
18
+ it("should store and retrieve flow jobs", async () => {
19
+ const mockFlowJob: FlowJob = {
20
+ id: "job1",
21
+ flowId: "flow1",
22
+ status: "running",
23
+ tasks: [],
24
+ createdAt: new Date(),
25
+ updatedAt: new Date(),
26
+ };
27
+
28
+ const mockStub = {
29
+ getFlowJob: vi.fn().mockResolvedValue(mockFlowJob),
30
+ setFlowJob: vi.fn().mockResolvedValue(undefined),
31
+ deleteFlowJob: vi.fn().mockResolvedValue(undefined),
32
+ emit: vi.fn().mockResolvedValue(undefined),
33
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
34
+
35
+ const mockDO = {
36
+ idFromName: vi.fn().mockReturnValue("id1"),
37
+ get: vi.fn().mockReturnValue(mockStub),
38
+ } as unknown as FlowJobDurableObject<FlowJob>;
39
+
40
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
41
+
42
+ await Effect.runPromise(
43
+ Effect.gen(function* () {
44
+ yield* store.set("job1", mockFlowJob);
45
+ const retrieved = yield* store.get("job1");
46
+ expect(retrieved).toEqual(mockFlowJob);
47
+ expect(mockDO.idFromName).toHaveBeenCalledWith("job1");
48
+ expect(mockStub.setFlowJob).toHaveBeenCalledWith(mockFlowJob);
49
+ }),
50
+ );
51
+ });
52
+
53
+ it("should fail when getting non-existent flow job", async () => {
54
+ const mockStub = {
55
+ getFlowJob: vi.fn().mockResolvedValue(undefined),
56
+ setFlowJob: vi.fn(),
57
+ deleteFlowJob: vi.fn(),
58
+ emit: vi.fn(),
59
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
60
+
61
+ const mockDO = {
62
+ idFromName: vi.fn().mockReturnValue("id1"),
63
+ get: vi.fn().mockReturnValue(mockStub),
64
+ } as unknown as FlowJobDurableObject<FlowJob>;
65
+
66
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
67
+
68
+ await Effect.runPromise(
69
+ Effect.gen(function* () {
70
+ const result = yield* Effect.either(store.get("non-existent"));
71
+ expect(result._tag).toBe("Left");
72
+ }),
73
+ );
74
+ });
75
+
76
+ it("should delete flow jobs", async () => {
77
+ const mockStub = {
78
+ getFlowJob: vi.fn().mockResolvedValue(undefined),
79
+ setFlowJob: vi.fn().mockResolvedValue(undefined),
80
+ deleteFlowJob: vi.fn().mockResolvedValue(undefined),
81
+ emit: vi.fn(),
82
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
83
+
84
+ const mockDO = {
85
+ idFromName: vi.fn().mockReturnValue("id1"),
86
+ get: vi.fn().mockReturnValue(mockStub),
87
+ } as unknown as FlowJobDurableObject<FlowJob>;
88
+
89
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
90
+
91
+ await Effect.runPromise(
92
+ Effect.gen(function* () {
93
+ yield* store.delete("job1");
94
+ expect(mockStub.deleteFlowJob).toHaveBeenCalled();
95
+ }),
96
+ );
97
+ });
98
+ });
99
+
100
+ describe("FlowJob Store - Complex Operations", () => {
101
+ it("should handle flow job updates", async () => {
102
+ const mockFlowJob1: FlowJob = {
103
+ id: "job1",
104
+ flowId: "flow1",
105
+ status: "running",
106
+ tasks: [],
107
+ createdAt: new Date(),
108
+ updatedAt: new Date(),
109
+ };
110
+
111
+ const mockFlowJob2: FlowJob = {
112
+ ...mockFlowJob1,
113
+ status: "completed",
114
+ };
115
+
116
+ const mockStub = {
117
+ getFlowJob: vi.fn().mockResolvedValue(mockFlowJob2),
118
+ setFlowJob: vi.fn().mockResolvedValue(undefined),
119
+ deleteFlowJob: vi.fn(),
120
+ emit: vi.fn(),
121
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
122
+
123
+ const mockDO = {
124
+ idFromName: vi.fn().mockReturnValue("id1"),
125
+ get: vi.fn().mockReturnValue(mockStub),
126
+ } as unknown as FlowJobDurableObject<FlowJob>;
127
+
128
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
129
+
130
+ await Effect.runPromise(
131
+ Effect.gen(function* () {
132
+ yield* store.set("job1", mockFlowJob1);
133
+ yield* store.set("job1", mockFlowJob2);
134
+ const retrieved = yield* store.get("job1");
135
+ expect(retrieved.status).toBe("completed");
136
+ expect(mockStub.setFlowJob).toHaveBeenCalledTimes(2);
137
+ }),
138
+ );
139
+ });
140
+
141
+ it("should handle flow jobs with tasks", async () => {
142
+ const mockFlowJob: FlowJob = {
143
+ id: "job1",
144
+ flowId: "flow1",
145
+ status: "running",
146
+ tasks: [
147
+ { id: "task1", status: "completed" },
148
+ { id: "task2", status: "running" },
149
+ ],
150
+ createdAt: new Date(),
151
+ updatedAt: new Date(),
152
+ };
153
+
154
+ const mockStub = {
155
+ getFlowJob: vi.fn().mockResolvedValue(mockFlowJob),
156
+ setFlowJob: vi.fn().mockResolvedValue(undefined),
157
+ deleteFlowJob: vi.fn(),
158
+ emit: vi.fn(),
159
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
160
+
161
+ const mockDO = {
162
+ idFromName: vi.fn().mockReturnValue("id1"),
163
+ get: vi.fn().mockReturnValue(mockStub),
164
+ } as unknown as FlowJobDurableObject<FlowJob>;
165
+
166
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
167
+
168
+ await Effect.runPromise(
169
+ Effect.gen(function* () {
170
+ yield* store.set("job1", mockFlowJob);
171
+ const retrieved = yield* store.get("job1");
172
+ expect(retrieved.tasks).toHaveLength(2);
173
+ expect(retrieved.tasks[0].id).toBe("task1");
174
+ }),
175
+ );
176
+ });
177
+ });
178
+
179
+ describe("FlowJob Store - Error Handling", () => {
180
+ it("should handle get errors", async () => {
181
+ const mockStub = {
182
+ getFlowJob: vi.fn().mockRejectedValue(new Error("Network error")),
183
+ setFlowJob: vi.fn(),
184
+ deleteFlowJob: vi.fn(),
185
+ emit: vi.fn(),
186
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
187
+
188
+ const mockDO = {
189
+ idFromName: vi.fn().mockReturnValue("id1"),
190
+ get: vi.fn().mockReturnValue(mockStub),
191
+ } as unknown as FlowJobDurableObject<FlowJob>;
192
+
193
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
194
+
195
+ await Effect.runPromise(
196
+ Effect.gen(function* () {
197
+ const result = yield* Effect.either(store.get("job1"));
198
+ expect(result._tag).toBe("Left");
199
+ }),
200
+ );
201
+ });
202
+
203
+ it("should handle set errors", async () => {
204
+ const mockFlowJob: FlowJob = {
205
+ id: "job1",
206
+ flowId: "flow1",
207
+ status: "running",
208
+ tasks: [],
209
+ createdAt: new Date(),
210
+ updatedAt: new Date(),
211
+ };
212
+
213
+ const mockStub = {
214
+ getFlowJob: vi.fn(),
215
+ setFlowJob: vi.fn().mockRejectedValue(new Error("Storage error")),
216
+ deleteFlowJob: vi.fn(),
217
+ emit: vi.fn(),
218
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
219
+
220
+ const mockDO = {
221
+ idFromName: vi.fn().mockReturnValue("id1"),
222
+ get: vi.fn().mockReturnValue(mockStub),
223
+ } as unknown as FlowJobDurableObject<FlowJob>;
224
+
225
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
226
+
227
+ await Effect.runPromise(
228
+ Effect.gen(function* () {
229
+ const result = yield* Effect.either(store.set("job1", mockFlowJob));
230
+ expect(result._tag).toBe("Left");
231
+ }),
232
+ );
233
+ });
234
+
235
+ it("should handle delete errors", async () => {
236
+ const mockStub = {
237
+ getFlowJob: vi.fn(),
238
+ setFlowJob: vi.fn(),
239
+ deleteFlowJob: vi.fn().mockRejectedValue(new Error("Delete failed")),
240
+ emit: vi.fn(),
241
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
242
+
243
+ const mockDO = {
244
+ idFromName: vi.fn().mockReturnValue("id1"),
245
+ get: vi.fn().mockReturnValue(mockStub),
246
+ } as unknown as FlowJobDurableObject<FlowJob>;
247
+
248
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
249
+
250
+ await Effect.runPromise(
251
+ Effect.gen(function* () {
252
+ const result = yield* Effect.either(store.delete("job1"));
253
+ expect(result._tag).toBe("Left");
254
+ }),
255
+ );
256
+ });
257
+ });
258
+
259
+ describe("FlowJob Store - Isolation", () => {
260
+ it("should create separate stubs for different keys", async () => {
261
+ const mockStub1 = {
262
+ getFlowJob: vi.fn().mockResolvedValue({
263
+ id: "job1",
264
+ flowId: "flow1",
265
+ status: "running",
266
+ tasks: [],
267
+ createdAt: new Date(),
268
+ updatedAt: new Date(),
269
+ }),
270
+ setFlowJob: vi.fn(),
271
+ deleteFlowJob: vi.fn(),
272
+ emit: vi.fn(),
273
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
274
+
275
+ const mockStub2 = {
276
+ getFlowJob: vi.fn().mockResolvedValue({
277
+ id: "job2",
278
+ flowId: "flow2",
279
+ status: "completed",
280
+ tasks: [],
281
+ createdAt: new Date(),
282
+ updatedAt: new Date(),
283
+ }),
284
+ setFlowJob: vi.fn(),
285
+ deleteFlowJob: vi.fn(),
286
+ emit: vi.fn(),
287
+ } as unknown as FlowJobDurableObjectBranded<FlowJob>;
288
+
289
+ const mockDO = {
290
+ idFromName: vi
291
+ .fn()
292
+ .mockReturnValueOnce("id1")
293
+ .mockReturnValueOnce("id2"),
294
+ get: vi.fn().mockReturnValueOnce(mockStub1).mockReturnValueOnce(mockStub2),
295
+ } as unknown as FlowJobDurableObject<FlowJob>;
296
+
297
+ const store = makeCloudflareDoFlowStore({ durableObject: mockDO });
298
+
299
+ await Effect.runPromise(
300
+ Effect.gen(function* () {
301
+ const job1 = yield* store.get("job1");
302
+ const job2 = yield* store.get("job2");
303
+
304
+ expect(job1.id).toBe("job1");
305
+ expect(job2.id).toBe("job2");
306
+ expect(mockDO.idFromName).toHaveBeenCalledWith("job1");
307
+ expect(mockDO.idFromName).toHaveBeenCalledWith("job2");
308
+ }),
309
+ );
310
+ });
311
+ });
312
+
313
+ describe("UploadFile Store - Basic Operations", () => {
314
+ it("should store and retrieve upload files", async () => {
315
+ const mockUploadFile: UploadFile = {
316
+ id: "file1",
317
+ offset: 0,
318
+ storage: { id: "s3", type: "s3" },
319
+ };
320
+
321
+ const mockStub = {
322
+ getUploadFile: vi.fn().mockResolvedValue(mockUploadFile),
323
+ setUploadFile: vi.fn().mockResolvedValue(undefined),
324
+ deleteUploadFile: vi.fn().mockResolvedValue(undefined),
325
+ emit: vi.fn().mockResolvedValue(undefined),
326
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
327
+
328
+ const mockDO = {
329
+ idFromName: vi.fn().mockReturnValue("id1"),
330
+ get: vi.fn().mockReturnValue(mockStub),
331
+ } as unknown as UploadFileDurableObject<UploadFile>;
332
+
333
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
334
+
335
+ await Effect.runPromise(
336
+ Effect.gen(function* () {
337
+ yield* store.set("file1", mockUploadFile);
338
+ const retrieved = yield* store.get("file1");
339
+ expect(retrieved).toEqual(mockUploadFile);
340
+ expect(mockDO.idFromName).toHaveBeenCalledWith("file1");
341
+ expect(mockStub.setUploadFile).toHaveBeenCalledWith(mockUploadFile);
342
+ }),
343
+ );
344
+ });
345
+
346
+ it("should fail when getting non-existent upload file", async () => {
347
+ const mockStub = {
348
+ getUploadFile: vi.fn().mockResolvedValue(undefined),
349
+ setUploadFile: vi.fn(),
350
+ deleteUploadFile: vi.fn(),
351
+ emit: vi.fn(),
352
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
353
+
354
+ const mockDO = {
355
+ idFromName: vi.fn().mockReturnValue("id1"),
356
+ get: vi.fn().mockReturnValue(mockStub),
357
+ } as unknown as UploadFileDurableObject<UploadFile>;
358
+
359
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
360
+
361
+ await Effect.runPromise(
362
+ Effect.gen(function* () {
363
+ const result = yield* Effect.either(store.get("non-existent"));
364
+ expect(result._tag).toBe("Left");
365
+ }),
366
+ );
367
+ });
368
+
369
+ it("should delete upload files", async () => {
370
+ const mockStub = {
371
+ getUploadFile: vi.fn().mockResolvedValue(undefined),
372
+ setUploadFile: vi.fn().mockResolvedValue(undefined),
373
+ deleteUploadFile: vi.fn().mockResolvedValue(undefined),
374
+ emit: vi.fn(),
375
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
376
+
377
+ const mockDO = {
378
+ idFromName: vi.fn().mockReturnValue("id1"),
379
+ get: vi.fn().mockReturnValue(mockStub),
380
+ } as unknown as UploadFileDurableObject<UploadFile>;
381
+
382
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
383
+
384
+ await Effect.runPromise(
385
+ Effect.gen(function* () {
386
+ yield* store.delete("file1");
387
+ expect(mockStub.deleteUploadFile).toHaveBeenCalled();
388
+ }),
389
+ );
390
+ });
391
+ });
392
+
393
+ describe("UploadFile Store - Complex Operations", () => {
394
+ it("should handle upload progress updates", async () => {
395
+ const mockUploadFile1: UploadFile = {
396
+ id: "file1",
397
+ offset: 0,
398
+ storage: { id: "s3", type: "s3" },
399
+ };
400
+
401
+ const mockUploadFile2: UploadFile = {
402
+ ...mockUploadFile1,
403
+ offset: 1024,
404
+ };
405
+
406
+ const mockStub = {
407
+ getUploadFile: vi.fn().mockResolvedValue(mockUploadFile2),
408
+ setUploadFile: vi.fn().mockResolvedValue(undefined),
409
+ deleteUploadFile: vi.fn(),
410
+ emit: vi.fn(),
411
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
412
+
413
+ const mockDO = {
414
+ idFromName: vi.fn().mockReturnValue("id1"),
415
+ get: vi.fn().mockReturnValue(mockStub),
416
+ } as unknown as UploadFileDurableObject<UploadFile>;
417
+
418
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
419
+
420
+ await Effect.runPromise(
421
+ Effect.gen(function* () {
422
+ yield* store.set("file1", mockUploadFile1);
423
+ yield* store.set("file1", mockUploadFile2);
424
+ const retrieved = yield* store.get("file1");
425
+ expect(retrieved.offset).toBe(1024);
426
+ expect(mockStub.setUploadFile).toHaveBeenCalledTimes(2);
427
+ }),
428
+ );
429
+ });
430
+
431
+ it("should handle upload files with metadata", async () => {
432
+ const mockUploadFile: UploadFile = {
433
+ id: "file1",
434
+ offset: 0,
435
+ storage: { id: "s3", type: "s3" },
436
+ size: 5000,
437
+ metadata: {
438
+ filename: "test.jpg",
439
+ contentType: "image/jpeg",
440
+ },
441
+ };
442
+
443
+ const mockStub = {
444
+ getUploadFile: vi.fn().mockResolvedValue(mockUploadFile),
445
+ setUploadFile: vi.fn().mockResolvedValue(undefined),
446
+ deleteUploadFile: vi.fn(),
447
+ emit: vi.fn(),
448
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
449
+
450
+ const mockDO = {
451
+ idFromName: vi.fn().mockReturnValue("id1"),
452
+ get: vi.fn().mockReturnValue(mockStub),
453
+ } as unknown as UploadFileDurableObject<UploadFile>;
454
+
455
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
456
+
457
+ await Effect.runPromise(
458
+ Effect.gen(function* () {
459
+ yield* store.set("file1", mockUploadFile);
460
+ const retrieved = yield* store.get("file1");
461
+ expect(retrieved.metadata?.filename).toBe("test.jpg");
462
+ expect(retrieved.size).toBe(5000);
463
+ }),
464
+ );
465
+ });
466
+ });
467
+
468
+ describe("UploadFile Store - Error Handling", () => {
469
+ it("should handle get errors", async () => {
470
+ const mockStub = {
471
+ getUploadFile: vi.fn().mockRejectedValue(new Error("Network error")),
472
+ setUploadFile: vi.fn(),
473
+ deleteUploadFile: vi.fn(),
474
+ emit: vi.fn(),
475
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
476
+
477
+ const mockDO = {
478
+ idFromName: vi.fn().mockReturnValue("id1"),
479
+ get: vi.fn().mockReturnValue(mockStub),
480
+ } as unknown as UploadFileDurableObject<UploadFile>;
481
+
482
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
483
+
484
+ await Effect.runPromise(
485
+ Effect.gen(function* () {
486
+ const result = yield* Effect.either(store.get("file1"));
487
+ expect(result._tag).toBe("Left");
488
+ }),
489
+ );
490
+ });
491
+
492
+ it("should handle set errors", async () => {
493
+ const mockUploadFile: UploadFile = {
494
+ id: "file1",
495
+ offset: 0,
496
+ storage: { id: "s3", type: "s3" },
497
+ };
498
+
499
+ const mockStub = {
500
+ getUploadFile: vi.fn(),
501
+ setUploadFile: vi.fn().mockRejectedValue(new Error("Storage error")),
502
+ deleteUploadFile: vi.fn(),
503
+ emit: vi.fn(),
504
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
505
+
506
+ const mockDO = {
507
+ idFromName: vi.fn().mockReturnValue("id1"),
508
+ get: vi.fn().mockReturnValue(mockStub),
509
+ } as unknown as UploadFileDurableObject<UploadFile>;
510
+
511
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
512
+
513
+ await Effect.runPromise(
514
+ Effect.gen(function* () {
515
+ const result = yield* Effect.either(
516
+ store.set("file1", mockUploadFile),
517
+ );
518
+ expect(result._tag).toBe("Left");
519
+ }),
520
+ );
521
+ });
522
+
523
+ it("should handle delete errors", async () => {
524
+ const mockStub = {
525
+ getUploadFile: vi.fn(),
526
+ setUploadFile: vi.fn(),
527
+ deleteUploadFile: vi.fn().mockRejectedValue(new Error("Delete failed")),
528
+ emit: vi.fn(),
529
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
530
+
531
+ const mockDO = {
532
+ idFromName: vi.fn().mockReturnValue("id1"),
533
+ get: vi.fn().mockReturnValue(mockStub),
534
+ } as unknown as UploadFileDurableObject<UploadFile>;
535
+
536
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
537
+
538
+ await Effect.runPromise(
539
+ Effect.gen(function* () {
540
+ const result = yield* Effect.either(store.delete("file1"));
541
+ expect(result._tag).toBe("Left");
542
+ }),
543
+ );
544
+ });
545
+ });
546
+
547
+ describe("UploadFile Store - Isolation", () => {
548
+ it("should create separate stubs for different keys", async () => {
549
+ const mockStub1 = {
550
+ getUploadFile: vi.fn().mockResolvedValue({
551
+ id: "file1",
552
+ offset: 0,
553
+ storage: { id: "s3", type: "s3" },
554
+ }),
555
+ setUploadFile: vi.fn(),
556
+ deleteUploadFile: vi.fn(),
557
+ emit: vi.fn(),
558
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
559
+
560
+ const mockStub2 = {
561
+ getUploadFile: vi.fn().mockResolvedValue({
562
+ id: "file2",
563
+ offset: 1024,
564
+ storage: { id: "gcs", type: "gcs" },
565
+ }),
566
+ setUploadFile: vi.fn(),
567
+ deleteUploadFile: vi.fn(),
568
+ emit: vi.fn(),
569
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
570
+
571
+ const mockDO = {
572
+ idFromName: vi
573
+ .fn()
574
+ .mockReturnValueOnce("id1")
575
+ .mockReturnValueOnce("id2"),
576
+ get: vi.fn().mockReturnValueOnce(mockStub1).mockReturnValueOnce(mockStub2),
577
+ } as unknown as UploadFileDurableObject<UploadFile>;
578
+
579
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
580
+
581
+ await Effect.runPromise(
582
+ Effect.gen(function* () {
583
+ const file1 = yield* store.get("file1");
584
+ const file2 = yield* store.get("file2");
585
+
586
+ expect(file1.id).toBe("file1");
587
+ expect(file2.id).toBe("file2");
588
+ expect(file1.storage.type).toBe("s3");
589
+ expect(file2.storage.type).toBe("gcs");
590
+ expect(mockDO.idFromName).toHaveBeenCalledWith("file1");
591
+ expect(mockDO.idFromName).toHaveBeenCalledWith("file2");
592
+ }),
593
+ );
594
+ });
595
+ });
596
+
597
+ describe("Durable Object Naming", () => {
598
+ it("should generate consistent IDs from names", async () => {
599
+ const mockStub = {
600
+ getUploadFile: vi.fn().mockResolvedValue({
601
+ id: "file1",
602
+ offset: 0,
603
+ storage: { id: "s3", type: "s3" },
604
+ }),
605
+ setUploadFile: vi.fn(),
606
+ deleteUploadFile: vi.fn(),
607
+ emit: vi.fn(),
608
+ } as unknown as UploadFileDurableObjectBranded<UploadFile>;
609
+
610
+ const mockDO = {
611
+ idFromName: vi.fn().mockReturnValue("consistent-id"),
612
+ get: vi.fn().mockReturnValue(mockStub),
613
+ } as unknown as UploadFileDurableObject<UploadFile>;
614
+
615
+ const store = makeCloudflareDoUploadStore({ durableObject: mockDO });
616
+
617
+ await Effect.runPromise(
618
+ Effect.gen(function* () {
619
+ yield* store.get("file1");
620
+ yield* store.get("file1");
621
+ yield* store.get("file1");
622
+
623
+ expect(mockDO.idFromName).toHaveBeenCalledTimes(3);
624
+ expect(mockDO.idFromName).toHaveBeenCalledWith("file1");
625
+ }),
626
+ );
627
+ });
628
+ });
629
+ });
@@ -0,0 +1,39 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ /**
4
+ * Shared vitest configuration template for uploadista-sdk packages
5
+ *
6
+ * This template should be used by all SDK packages to ensure consistent
7
+ * testing configuration across the monorepo.
8
+ *
9
+ * Key features:
10
+ * - Tests in dedicated `tests/` directories (not colocated with src)
11
+ * - Node environment for server-side code
12
+ * - V8 coverage provider
13
+ * - Global test functions available
14
+ * - Effect testing support via @effect/vitest
15
+ *
16
+ * Usage:
17
+ * Copy this file to your package root as `vitest.config.ts` and customize
18
+ * if needed (though most packages should use this as-is).
19
+ */
20
+ export default defineConfig({
21
+ test: {
22
+ globals: true,
23
+ environment: "node",
24
+ include: ["tests/**/*.test.ts"],
25
+ exclude: ["node_modules", "dist"],
26
+ coverage: {
27
+ provider: "v8",
28
+ reporter: ["text", "json", "html"],
29
+ exclude: [
30
+ "node_modules/",
31
+ "dist/",
32
+ "**/*.d.ts",
33
+ "**/*.test.ts",
34
+ "**/*.spec.ts",
35
+ "tests/",
36
+ ],
37
+ },
38
+ },
39
+ });