@uploadista/core 0.0.13-beta.5 → 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.
Files changed (101) hide show
  1. package/README.md +1 -1
  2. package/dist/{checksum-P9C2JlRk.mjs → checksum-CtOagryS.mjs} +2 -2
  3. package/dist/{checksum-P9C2JlRk.mjs.map → checksum-CtOagryS.mjs.map} +1 -1
  4. package/dist/errors/index.d.cts +2 -2
  5. package/dist/errors/index.d.mts +2 -2
  6. package/dist/errors/index.mjs +1 -1
  7. package/dist/flow/index.cjs +1 -1
  8. package/dist/flow/index.d.cts +5 -5
  9. package/dist/flow/index.d.mts +5 -5
  10. package/dist/flow/index.mjs +1 -1
  11. package/dist/{flow-DkTE3siV.cjs → flow-ChADffZ5.cjs} +1 -1
  12. package/dist/{flow-IgE8hj7H.mjs → flow-_J9-Dm_m.mjs} +2 -2
  13. package/dist/flow-_J9-Dm_m.mjs.map +1 -0
  14. package/dist/{index-CrZopnP9.d.cts → index-4VDJDcWM.d.cts} +227 -241
  15. package/dist/index-4VDJDcWM.d.cts.map +1 -0
  16. package/dist/{index-BPBI84iT.d.mts → index-Bi9YYid8.d.mts} +2 -2
  17. package/dist/{index-BPBI84iT.d.mts.map → index-Bi9YYid8.d.mts.map} +1 -1
  18. package/dist/{index-BteFEg-c.d.mts → index-Cbf1OPLp.d.mts} +2 -2
  19. package/dist/{index-BteFEg-c.d.mts.map → index-Cbf1OPLp.d.mts.map} +1 -1
  20. package/dist/{index-DMfADSSJ.d.cts → index-De4wQJwR.d.cts} +2 -2
  21. package/dist/{index-DMfADSSJ.d.cts.map → index-De4wQJwR.d.cts.map} +1 -1
  22. package/dist/{index-DHt7Ht_J.d.mts → index-RgOX4psL.d.mts} +305 -139
  23. package/dist/index-RgOX4psL.d.mts.map +1 -0
  24. package/dist/{index-DubOIur4.d.cts → index-qZ90PVNl.d.cts} +2 -2
  25. package/dist/index-qZ90PVNl.d.cts.map +1 -0
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.cts +5 -5
  28. package/dist/index.d.mts +5 -5
  29. package/dist/index.mjs +1 -1
  30. package/dist/{stream-limiter-DFtRZczp.mjs → stream-limiter-D9KSAaoY.mjs} +2 -2
  31. package/dist/{stream-limiter-DFtRZczp.mjs.map → stream-limiter-D9KSAaoY.mjs.map} +1 -1
  32. package/dist/streams/index.d.cts +2 -2
  33. package/dist/streams/index.d.mts +2 -2
  34. package/dist/streams/index.mjs +1 -1
  35. package/dist/testing/index.cjs +1 -0
  36. package/dist/testing/index.d.cts +110 -0
  37. package/dist/testing/index.d.cts.map +1 -0
  38. package/dist/testing/index.d.mts +110 -0
  39. package/dist/testing/index.d.mts.map +1 -0
  40. package/dist/testing/index.mjs +2 -0
  41. package/dist/testing/index.mjs.map +1 -0
  42. package/dist/types/index.d.cts +5 -5
  43. package/dist/types/index.d.mts +5 -5
  44. package/dist/types/index.mjs +1 -1
  45. package/dist/{types-DGZ892my.mjs → types-BI_KmpTc.mjs} +2 -2
  46. package/dist/types-BI_KmpTc.mjs.map +1 -0
  47. package/dist/upload/index.d.cts +5 -5
  48. package/dist/upload/index.d.mts +5 -5
  49. package/dist/upload/index.mjs +1 -1
  50. package/dist/{upload-DJTptYqV.mjs → upload-Yj5lrtZo.mjs} +2 -2
  51. package/dist/{upload-DJTptYqV.mjs.map → upload-Yj5lrtZo.mjs.map} +1 -1
  52. package/dist/{uploadista-error-9yLWP7TC.d.cts → uploadista-error-BQLhNZcY.d.cts} +1 -1
  53. package/dist/{uploadista-error-9yLWP7TC.d.cts.map → uploadista-error-BQLhNZcY.d.cts.map} +1 -1
  54. package/dist/{uploadista-error-nZ_q-EZy.mjs → uploadista-error-Buscq-FR.mjs} +1 -1
  55. package/dist/{uploadista-error-nZ_q-EZy.mjs.map → uploadista-error-Buscq-FR.mjs.map} +1 -1
  56. package/dist/{uploadista-error-CBkvsyZ3.d.mts → uploadista-error-DUWw6OqS.d.mts} +1 -1
  57. package/dist/{uploadista-error-CBkvsyZ3.d.mts.map → uploadista-error-DUWw6OqS.d.mts.map} +1 -1
  58. package/dist/utils/index.d.cts +2 -2
  59. package/dist/utils/index.d.mts +2 -2
  60. package/dist/utils/index.mjs +1 -1
  61. package/dist/{utils-BicUw_lt.mjs → utils-BWiu6lqv.mjs} +2 -2
  62. package/dist/{utils-BicUw_lt.mjs.map → utils-BWiu6lqv.mjs.map} +1 -1
  63. package/package.json +14 -6
  64. package/src/flow/node.ts +4 -4
  65. package/src/flow/nodes/transform-node.ts +23 -2
  66. package/src/flow/plugins/credential-provider.ts +1 -1
  67. package/src/flow/plugins/image-ai-plugin.ts +1 -1
  68. package/src/flow/plugins/image-plugin.ts +1 -1
  69. package/src/flow/plugins/video-plugin.ts +1 -1
  70. package/src/flow/plugins/zip-plugin.ts +1 -1
  71. package/src/flow/types/type-utils.ts +14 -3
  72. package/src/testing/index.ts +14 -0
  73. package/src/testing/mock-image-ai-plugin.ts +33 -0
  74. package/src/testing/mock-image-plugin.ts +56 -0
  75. package/src/testing/mock-upload-server.ts +176 -0
  76. package/src/testing/mock-video-plugin.ts +94 -0
  77. package/src/testing/mock-zip-plugin.ts +41 -0
  78. package/src/types/data-store.ts +1 -1
  79. package/{src/errors/__tests__ → tests/errors}/uploadista-error.test.ts +23 -19
  80. package/{src → tests}/flow/edge.test.ts +1 -1
  81. package/tests/flow/flow.test.ts +853 -0
  82. package/tests/flow/node.test.ts +757 -0
  83. package/{src → tests}/streams/stream-limiter.test.ts +2 -2
  84. package/tests/types/typed-event-emitter.test.ts +282 -0
  85. package/{src → tests}/utils/debounce.test.ts +1 -1
  86. package/{src → tests}/utils/once.test.ts +1 -1
  87. package/tests/utils/test-layers.ts +183 -0
  88. package/{src → tests}/utils/throttle.test.ts +1 -1
  89. package/tsdown.config.ts +1 -0
  90. package/type-tests/flow.test-d.ts +93 -0
  91. package/type-tests/type-utils.test-d.ts +104 -51
  92. package/vitest.config.ts +19 -1
  93. package/dist/flow-IgE8hj7H.mjs.map +0 -1
  94. package/dist/index-CrZopnP9.d.cts.map +0 -1
  95. package/dist/index-DHt7Ht_J.d.mts.map +0 -1
  96. package/dist/index-DubOIur4.d.cts.map +0 -1
  97. package/dist/types-DGZ892my.mjs.map +0 -1
  98. /package/dist/{errors-C0zLx77t.mjs → errors-DEFjN-xn.mjs} +0 -0
  99. /package/dist/{index-BtBZHVmz.d.cts → index-C-svZlpj.d.mts} +0 -0
  100. /package/dist/{index-DEHBdV_z.d.mts → index-_wQ5ClJU.d.cts} +0 -0
  101. /package/dist/{streams-CJKKIAwy.mjs → streams-DPU17bYp.mjs} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { UploadistaError } from "../errors/uploadista-error";
3
- import { streamLimiter } from "./stream-limiter";
2
+ import { UploadistaError } from "../../src/errors";
3
+ import { streamLimiter } from "../../src/streams/stream-limiter";
4
4
 
5
5
  // Helper function to convert stream to array
6
6
  async function streamToArray(
@@ -0,0 +1,282 @@
1
+ import type { WebSocketConnection } from "@uploadista/core/types";
2
+ import {
3
+ type BaseEventEmitter,
4
+ TypedEventEmitter,
5
+ eventToMessageSerializer,
6
+ } from "@uploadista/core/types";
7
+ import { Effect } from "effect";
8
+ import { describe, expect, it, vi } from "vitest";
9
+
10
+ interface TestEvent {
11
+ id: string;
12
+ type: string;
13
+ data: string;
14
+ }
15
+
16
+ class MockWebSocketConnection implements WebSocketConnection {
17
+ id = "test-conn";
18
+ readyState = 1;
19
+ sentMessages: string[] = [];
20
+
21
+ send(data: string): void {
22
+ this.sentMessages.push(data);
23
+ }
24
+
25
+ close(): void {
26
+ this.readyState = 3;
27
+ }
28
+ }
29
+
30
+ describe("TypedEventEmitter", () => {
31
+ describe("Event Serialization", () => {
32
+ it("should serialize event using eventToMessage function", async () => {
33
+ const emittedMessages: Array<{ key: string; message: string }> = [];
34
+ const mockBaseEmitter: BaseEventEmitter = {
35
+ emit: (key: string, message: string) =>
36
+ Effect.sync(() => {
37
+ emittedMessages.push({ key, message });
38
+ }),
39
+ subscribe: () => Effect.void,
40
+ unsubscribe: () => Effect.void,
41
+ };
42
+
43
+ const serializer = (event: TestEvent) =>
44
+ JSON.stringify({ custom: "format", event });
45
+
46
+ const typedEmitter = new TypedEventEmitter(mockBaseEmitter, serializer);
47
+
48
+ await Effect.runPromise(
49
+ Effect.gen(function* () {
50
+ const event: TestEvent = { id: "1", type: "test", data: "hello" };
51
+ yield* typedEmitter.emit("key1", event);
52
+
53
+ expect(emittedMessages).toHaveLength(1);
54
+ expect(emittedMessages[0].key).toBe("key1");
55
+
56
+ const parsed = JSON.parse(emittedMessages[0].message);
57
+ expect(parsed.custom).toBe("format");
58
+ expect(parsed.event).toEqual(event);
59
+ }),
60
+ );
61
+ });
62
+
63
+ it("should delegate emit to BaseEventEmitter with serialized message", async () => {
64
+ const emitSpy = vi.fn().mockReturnValue(Effect.void);
65
+ const mockBaseEmitter: BaseEventEmitter = {
66
+ emit: emitSpy,
67
+ subscribe: () => Effect.void,
68
+ unsubscribe: () => Effect.void,
69
+ };
70
+
71
+ const typedEmitter = new TypedEventEmitter(
72
+ mockBaseEmitter,
73
+ (event: TestEvent) => JSON.stringify(event),
74
+ );
75
+
76
+ await Effect.runPromise(
77
+ Effect.gen(function* () {
78
+ const event: TestEvent = { id: "2", type: "action", data: "world" };
79
+ yield* typedEmitter.emit("key2", event);
80
+
81
+ expect(emitSpy).toHaveBeenCalledWith("key2", JSON.stringify(event));
82
+ expect(emitSpy).toHaveBeenCalledTimes(1);
83
+ }),
84
+ );
85
+ });
86
+
87
+ it("should delegate subscribe to BaseEventEmitter", async () => {
88
+ const subscribeSpy = vi.fn().mockReturnValue(Effect.void);
89
+ const mockBaseEmitter: BaseEventEmitter = {
90
+ emit: () => Effect.void,
91
+ subscribe: subscribeSpy,
92
+ unsubscribe: () => Effect.void,
93
+ };
94
+
95
+ const typedEmitter = new TypedEventEmitter(
96
+ mockBaseEmitter,
97
+ (event: TestEvent) => JSON.stringify(event),
98
+ );
99
+
100
+ await Effect.runPromise(
101
+ Effect.gen(function* () {
102
+ const connection = new MockWebSocketConnection();
103
+ yield* typedEmitter.subscribe("key3", connection);
104
+
105
+ expect(subscribeSpy).toHaveBeenCalledWith("key3", connection);
106
+ expect(subscribeSpy).toHaveBeenCalledTimes(1);
107
+ }),
108
+ );
109
+ });
110
+
111
+ it("should delegate unsubscribe to BaseEventEmitter", async () => {
112
+ const unsubscribeSpy = vi.fn().mockReturnValue(Effect.void);
113
+ const mockBaseEmitter: BaseEventEmitter = {
114
+ emit: () => Effect.void,
115
+ subscribe: () => Effect.void,
116
+ unsubscribe: unsubscribeSpy,
117
+ };
118
+
119
+ const typedEmitter = new TypedEventEmitter(
120
+ mockBaseEmitter,
121
+ (event: TestEvent) => JSON.stringify(event),
122
+ );
123
+
124
+ await Effect.runPromise(
125
+ Effect.gen(function* () {
126
+ yield* typedEmitter.unsubscribe("key4");
127
+
128
+ expect(unsubscribeSpy).toHaveBeenCalledWith("key4");
129
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
130
+ }),
131
+ );
132
+ });
133
+ });
134
+
135
+ describe("Standard Serializers", () => {
136
+ it("should produce correct format for upload_event", async () => {
137
+ const emittedMessages: string[] = [];
138
+ const mockBaseEmitter: BaseEventEmitter = {
139
+ emit: (key: string, message: string) =>
140
+ Effect.sync(() => {
141
+ emittedMessages.push(message);
142
+ }),
143
+ subscribe: () => Effect.void,
144
+ unsubscribe: () => Effect.void,
145
+ };
146
+
147
+ const serializer = eventToMessageSerializer("upload_event");
148
+ const typedEmitter = new TypedEventEmitter(
149
+ mockBaseEmitter,
150
+ serializer.eventToMessage,
151
+ );
152
+
153
+ await Effect.runPromise(
154
+ Effect.gen(function* () {
155
+ const event: TestEvent = { id: "upload-1", type: "progress", data: "50%" };
156
+ yield* typedEmitter.emit("upload-key", event);
157
+
158
+ expect(emittedMessages).toHaveLength(1);
159
+
160
+ const parsed = JSON.parse(emittedMessages[0]);
161
+ expect(parsed.type).toBe("upload_event");
162
+ expect(parsed.payload).toEqual(event);
163
+ expect(parsed.timestamp).toBeDefined();
164
+ expect(typeof parsed.timestamp).toBe("string");
165
+ }),
166
+ );
167
+ });
168
+
169
+ it("should produce correct format for flow_event", async () => {
170
+ const emittedMessages: string[] = [];
171
+ const mockBaseEmitter: BaseEventEmitter = {
172
+ emit: (key: string, message: string) =>
173
+ Effect.sync(() => {
174
+ emittedMessages.push(message);
175
+ }),
176
+ subscribe: () => Effect.void,
177
+ unsubscribe: () => Effect.void,
178
+ };
179
+
180
+ const serializer = eventToMessageSerializer("flow_event");
181
+ const typedEmitter = new TypedEventEmitter(
182
+ mockBaseEmitter,
183
+ serializer.eventToMessage,
184
+ );
185
+
186
+ await Effect.runPromise(
187
+ Effect.gen(function* () {
188
+ const event: TestEvent = { id: "flow-1", type: "complete", data: "done" };
189
+ yield* typedEmitter.emit("flow-key", event);
190
+
191
+ expect(emittedMessages).toHaveLength(1);
192
+
193
+ const parsed = JSON.parse(emittedMessages[0]);
194
+ expect(parsed.type).toBe("flow_event");
195
+ expect(parsed.payload).toEqual(event);
196
+ expect(parsed.timestamp).toBeDefined();
197
+ }),
198
+ );
199
+ });
200
+
201
+ it("should include valid ISO 8601 timestamp", async () => {
202
+ const emittedMessages: string[] = [];
203
+ const mockBaseEmitter: BaseEventEmitter = {
204
+ emit: (key: string, message: string) =>
205
+ Effect.sync(() => {
206
+ emittedMessages.push(message);
207
+ }),
208
+ subscribe: () => Effect.void,
209
+ unsubscribe: () => Effect.void,
210
+ };
211
+
212
+ const serializer = eventToMessageSerializer("upload_event");
213
+ const typedEmitter = new TypedEventEmitter(
214
+ mockBaseEmitter,
215
+ serializer.eventToMessage,
216
+ );
217
+
218
+ await Effect.runPromise(
219
+ Effect.gen(function* () {
220
+ const event: TestEvent = { id: "test", type: "test", data: "test" };
221
+ yield* typedEmitter.emit("test-key", event);
222
+
223
+ const parsed = JSON.parse(emittedMessages[0]);
224
+ const timestamp = new Date(parsed.timestamp);
225
+
226
+ expect(timestamp.toISOString()).toBe(parsed.timestamp);
227
+ expect(isNaN(timestamp.getTime())).toBe(false);
228
+ }),
229
+ );
230
+ });
231
+ });
232
+
233
+ describe("Custom Serializers", () => {
234
+ it("should use custom eventToMessage function", async () => {
235
+ const emittedMessages: string[] = [];
236
+ const mockBaseEmitter: BaseEventEmitter = {
237
+ emit: (key: string, message: string) =>
238
+ Effect.sync(() => {
239
+ emittedMessages.push(message);
240
+ }),
241
+ subscribe: () => Effect.void,
242
+ unsubscribe: () => Effect.void,
243
+ };
244
+
245
+ const customSerializer = (event: TestEvent) => `CUSTOM:${event.id}:${event.type}:${event.data}`;
246
+
247
+ const typedEmitter = new TypedEventEmitter(mockBaseEmitter, customSerializer);
248
+
249
+ await Effect.runPromise(
250
+ Effect.gen(function* () {
251
+ const event: TestEvent = { id: "123", type: "action", data: "payload" };
252
+ yield* typedEmitter.emit("custom-key", event);
253
+
254
+ expect(emittedMessages).toHaveLength(1);
255
+ expect(emittedMessages[0]).toBe("CUSTOM:123:action:payload");
256
+ }),
257
+ );
258
+ });
259
+
260
+ it("should pass custom serializer output to BaseEventEmitter", async () => {
261
+ const emitSpy = vi.fn().mockReturnValue(Effect.void);
262
+ const mockBaseEmitter: BaseEventEmitter = {
263
+ emit: emitSpy,
264
+ subscribe: () => Effect.void,
265
+ unsubscribe: () => Effect.void,
266
+ };
267
+
268
+ const customSerializer = (event: TestEvent) => `EVENT-${event.id}`;
269
+
270
+ const typedEmitter = new TypedEventEmitter(mockBaseEmitter, customSerializer);
271
+
272
+ await Effect.runPromise(
273
+ Effect.gen(function* () {
274
+ const event: TestEvent = { id: "456", type: "test", data: "data" };
275
+ yield* typedEmitter.emit("key", event);
276
+
277
+ expect(emitSpy).toHaveBeenCalledWith("key", "EVENT-456");
278
+ }),
279
+ );
280
+ });
281
+ });
282
+ });
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { debounce } from "./debounce";
2
+ import { debounce } from "../../src/utils/debounce";
3
3
 
4
4
  describe("debounce", () => {
5
5
  beforeEach(() => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
- import { once } from "./once";
2
+ import { once } from "../../src/utils/once";
3
3
 
4
4
  describe("once", () => {
5
5
  it("should call the function only once", () => {
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Test Utilities for Effect-based Testing
3
+ *
4
+ * This file provides reusable test utilities and Layer mocking patterns
5
+ * for testing Effect-based code in the uploadista-sdk.
6
+ *
7
+ * ## Effect Testing Patterns
8
+ *
9
+ * ### 1. Using it.effect()
10
+ * For testing Effect operations with automatic TestContext provision:
11
+ *
12
+ * ```typescript
13
+ * import { it } from "@effect/vitest";
14
+ * import { Effect } from "effect";
15
+ *
16
+ * it.effect("should process data", () =>
17
+ * Effect.gen(function* () {
18
+ * const result = yield* processData();
19
+ * expect(result).toBe(expected);
20
+ * })
21
+ * );
22
+ * ```
23
+ *
24
+ * ### 2. Using TestClock for Time Control
25
+ * For testing timeouts, delays, and scheduled operations:
26
+ *
27
+ * ```typescript
28
+ * import { it } from "@effect/vitest";
29
+ * import { Effect, TestClock } from "effect";
30
+ *
31
+ * it.effect("should handle timeout", () =>
32
+ * Effect.gen(function* () {
33
+ * const fiber = yield* Effect.timeout(operation, "5 seconds").pipe(
34
+ * Effect.fork
35
+ * );
36
+ *
37
+ * // Advance time without real delay
38
+ * yield* TestClock.adjust("6 seconds");
39
+ *
40
+ * const result = yield* Fiber.join(fiber);
41
+ * expect(result).toBe(None);
42
+ * })
43
+ * );
44
+ * ```
45
+ *
46
+ * ### 3. Using Layer.mock() for Service Mocking
47
+ * For mocking Effect services with partial implementations:
48
+ *
49
+ * ```typescript
50
+ * import { Layer, Context } from "effect";
51
+ *
52
+ * // Define Test layer (mock)
53
+ * const DataStoreTest = Layer.succeed(
54
+ * DataStore,
55
+ * DataStore.of({
56
+ * upload: () => Effect.succeed({ key: "test-key" }),
57
+ * download: () => Effect.fail(new Error("Not implemented")),
58
+ * })
59
+ * );
60
+ *
61
+ * // Use in tests
62
+ * it.effect("should upload file", () =>
63
+ * Effect.gen(function* () {
64
+ * const store = yield* DataStore;
65
+ * const result = yield* store.upload(file);
66
+ * expect(result.key).toBe("test-key");
67
+ * }).pipe(Effect.provide(DataStoreTest))
68
+ * );
69
+ * ```
70
+ *
71
+ * ### 4. Using it.scoped() for Resource Management
72
+ * For tests requiring Scope lifecycle management:
73
+ *
74
+ * ```typescript
75
+ * import { it } from "@effect/vitest";
76
+ * import { Effect } from "effect";
77
+ *
78
+ * it.scoped("should manage resources", () =>
79
+ * Effect.gen(function* () {
80
+ * const resource = yield* acquireResource();
81
+ * // Resource will be automatically released after test
82
+ * yield* useResource(resource);
83
+ * })
84
+ * );
85
+ * ```
86
+ *
87
+ * ### 5. Test vs Live Layer Naming Convention
88
+ * Follow Effect's convention for layer naming:
89
+ * - `ServiceNameLive` - Production layer with real implementation
90
+ * - `ServiceNameTest` - Test layer with mocked implementation
91
+ *
92
+ * Example:
93
+ * ```typescript
94
+ * export const S3StoreLive = Layer.effect(...) // Real S3 client
95
+ * export const S3StoreTest = Layer.succeed(...) // Mocked S3 client
96
+ * ```
97
+ */
98
+
99
+ import { Context, Effect, Layer } from "effect";
100
+
101
+ /**
102
+ * Example: Mock Data Store Layer
103
+ *
104
+ * This demonstrates creating a mock layer for a data store service.
105
+ * Replace with actual service interfaces as needed.
106
+ */
107
+
108
+ // Example service interface (replace with actual interfaces from core)
109
+ export interface MockDataStore {
110
+ readonly upload: (data: Uint8Array) => Effect.Effect<{ key: string }, Error>;
111
+ readonly download: (key: string) => Effect.Effect<Uint8Array, Error>;
112
+ readonly delete: (key: string) => Effect.Effect<void, Error>;
113
+ }
114
+
115
+ export const MockDataStore = Context.GenericTag<MockDataStore>(
116
+ "@uploadista/MockDataStore",
117
+ );
118
+
119
+ /**
120
+ * Test layer for MockDataStore with in-memory storage
121
+ */
122
+ export const MockDataStoreTest = Layer.succeed(
123
+ MockDataStore,
124
+ MockDataStore.of({
125
+ upload: (data: Uint8Array) =>
126
+ Effect.succeed({ key: `mock-key-${data.length}` }),
127
+ download: (_key: string) =>
128
+ Effect.succeed(new Uint8Array([1, 2, 3])) as Effect.Effect<
129
+ Uint8Array,
130
+ Error
131
+ >,
132
+ delete: (_key: string) => Effect.void,
133
+ }),
134
+ );
135
+
136
+ /**
137
+ * Example: Mock KV Store Layer
138
+ */
139
+ export interface MockKVStore {
140
+ readonly get: (key: string) => Effect.Effect<string | null, Error>;
141
+ readonly set: (
142
+ key: string,
143
+ value: string,
144
+ ttl?: number,
145
+ ) => Effect.Effect<void, Error>;
146
+ readonly delete: (key: string) => Effect.Effect<void, Error>;
147
+ }
148
+
149
+ export const MockKVStore = Context.GenericTag<MockKVStore>(
150
+ "@uploadista/MockKVStore",
151
+ );
152
+
153
+ /**
154
+ * Test layer for MockKVStore with in-memory Map
155
+ */
156
+ export const MockKVStoreTest = (() => {
157
+ const store = new Map<string, string>();
158
+
159
+ return Layer.succeed(
160
+ MockKVStore,
161
+ MockKVStore.of({
162
+ get: (key: string) => Effect.succeed(store.get(key) ?? null),
163
+ set: (key: string, value: string) =>
164
+ Effect.sync(() => {
165
+ store.set(key, value);
166
+ }),
167
+ delete: (key: string) =>
168
+ Effect.sync(() => {
169
+ store.delete(key);
170
+ }),
171
+ }),
172
+ );
173
+ })();
174
+
175
+ /**
176
+ * Utility to create a Layer that fails (useful for testing error scenarios)
177
+ */
178
+ export const createFailingLayer = <T>(
179
+ _tag: Context.Tag<T, T>,
180
+ error: Error,
181
+ ): Layer.Layer<T, Error, never> => {
182
+ return Layer.fail(error);
183
+ };
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { throttle } from "./throttle";
2
+ import { throttle } from "../../src/utils/throttle";
3
3
 
4
4
  describe("throttle", () => {
5
5
  beforeEach(() => {
package/tsdown.config.ts CHANGED
@@ -9,6 +9,7 @@ export default defineConfig({
9
9
  "upload/index": "src/upload/index.ts",
10
10
  "streams/index": "src/streams/index.ts",
11
11
  "utils/index": "src/utils/index.ts",
12
+ "testing/index": "src/testing/index.ts",
12
13
  },
13
14
  minify: true,
14
15
  format: ["esm", "cjs"],
@@ -0,0 +1,93 @@
1
+ import { Effect } from "effect";
2
+ import { expectType } from "tsd";
3
+ import { z } from "zod";
4
+ import {
5
+ createFlow,
6
+ createFlowNode,
7
+ type FlowInputMap,
8
+ type FlowOutputMap,
9
+ NodeType,
10
+ } from "../src/flow";
11
+ import type { ResolveEffect } from "../src/flow/types/type-utils";
12
+
13
+ // Test 1: Verify node type preservation
14
+ const inputNode = createFlowNode({
15
+ id: "input-node",
16
+ name: "Input Node",
17
+ description: "Input Node",
18
+ type: NodeType.input,
19
+ inputSchema: z.object({ value: z.string() }),
20
+ outputSchema: z.object({ result: z.string() }),
21
+ run: ({ data }) =>
22
+ Effect.succeed({ type: "complete", data: { result: data.value } }),
23
+ });
24
+
25
+ type TestInputNode = ResolveEffect<typeof inputNode>;
26
+
27
+ // The node's type field should be the literal "input", not the union NodeType
28
+ expectType<NodeType.input>(({} as TestInputNode).type);
29
+
30
+ const outputNode = createFlowNode({
31
+ id: "output-node",
32
+ name: "Output Node",
33
+ description: "Output Node",
34
+ type: NodeType.output,
35
+ inputSchema: z.object({ result: z.string() }),
36
+ outputSchema: z.object({ value: z.string() }),
37
+ run: ({ data }) =>
38
+ Effect.succeed({ type: "complete", data: { value: data.result } }),
39
+ });
40
+
41
+ type TestOutputNode = ResolveEffect<typeof outputNode>;
42
+
43
+ // The node's type field should be the literal "output", not the union NodeType
44
+ expectType<NodeType.output>(({} as TestOutputNode).type);
45
+
46
+ // Test 2: Verify FlowInputMap and FlowOutputMap
47
+ const nodes = {
48
+ input: inputNode,
49
+ output: outputNode,
50
+ };
51
+
52
+ type NodesType = typeof nodes;
53
+
54
+ // FlowInputMap should only include keys of input nodes
55
+ type InputMapTest = FlowInputMap<NodesType>;
56
+ expectType<{
57
+ input: { value: string };
58
+ }>({} as InputMapTest);
59
+
60
+ // FlowOutputMap should only include keys of output nodes
61
+ type OutputMapTest = FlowOutputMap<NodesType>;
62
+ expectType<{
63
+ output: { value: string };
64
+ }>({} as OutputMapTest);
65
+
66
+ // Test 3: Verify typed flow
67
+ const flow = createFlow({
68
+ flowId: "test-flow",
69
+ name: "Test Flow",
70
+ nodes,
71
+ edges: [{ source: "input", target: "output" }],
72
+ });
73
+
74
+ // Test that run() returns the correct output type
75
+ Effect.runPromise(
76
+ Effect.gen(function* () {
77
+ const typedFlow = yield* flow;
78
+ const result = yield* typedFlow.run({
79
+ inputs: { input: { value: "test" } },
80
+ storageId: "storage-1",
81
+ jobId: "job-1",
82
+ });
83
+
84
+ if (result.type === "completed") {
85
+ // result.result should be FlowOutputMap<NodesType>
86
+ // Which means it should have an "output" key with the output schema
87
+ expectType<{ output: { value: string } }>(result.result);
88
+
89
+ // We should be able to access output node result
90
+ expectType<{ value: string }>(result.result.output);
91
+ }
92
+ }),
93
+ );