@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.
- package/README.md +1 -1
- package/dist/{checksum-P9C2JlRk.mjs → checksum-CtOagryS.mjs} +2 -2
- package/dist/{checksum-P9C2JlRk.mjs.map → checksum-CtOagryS.mjs.map} +1 -1
- package/dist/errors/index.d.cts +2 -2
- package/dist/errors/index.d.mts +2 -2
- package/dist/errors/index.mjs +1 -1
- package/dist/flow/index.cjs +1 -1
- package/dist/flow/index.d.cts +5 -5
- package/dist/flow/index.d.mts +5 -5
- package/dist/flow/index.mjs +1 -1
- package/dist/{flow-DkTE3siV.cjs → flow-ChADffZ5.cjs} +1 -1
- package/dist/{flow-IgE8hj7H.mjs → flow-_J9-Dm_m.mjs} +2 -2
- package/dist/flow-_J9-Dm_m.mjs.map +1 -0
- package/dist/{index-CrZopnP9.d.cts → index-4VDJDcWM.d.cts} +227 -241
- package/dist/index-4VDJDcWM.d.cts.map +1 -0
- package/dist/{index-BPBI84iT.d.mts → index-Bi9YYid8.d.mts} +2 -2
- package/dist/{index-BPBI84iT.d.mts.map → index-Bi9YYid8.d.mts.map} +1 -1
- package/dist/{index-BteFEg-c.d.mts → index-Cbf1OPLp.d.mts} +2 -2
- package/dist/{index-BteFEg-c.d.mts.map → index-Cbf1OPLp.d.mts.map} +1 -1
- package/dist/{index-DMfADSSJ.d.cts → index-De4wQJwR.d.cts} +2 -2
- package/dist/{index-DMfADSSJ.d.cts.map → index-De4wQJwR.d.cts.map} +1 -1
- package/dist/{index-DHt7Ht_J.d.mts → index-RgOX4psL.d.mts} +305 -139
- package/dist/index-RgOX4psL.d.mts.map +1 -0
- package/dist/{index-DubOIur4.d.cts → index-qZ90PVNl.d.cts} +2 -2
- package/dist/index-qZ90PVNl.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +1 -1
- package/dist/{stream-limiter-DFtRZczp.mjs → stream-limiter-D9KSAaoY.mjs} +2 -2
- package/dist/{stream-limiter-DFtRZczp.mjs.map → stream-limiter-D9KSAaoY.mjs.map} +1 -1
- package/dist/streams/index.d.cts +2 -2
- package/dist/streams/index.d.mts +2 -2
- package/dist/streams/index.mjs +1 -1
- package/dist/testing/index.cjs +1 -0
- package/dist/testing/index.d.cts +110 -0
- package/dist/testing/index.d.cts.map +1 -0
- package/dist/testing/index.d.mts +110 -0
- package/dist/testing/index.d.mts.map +1 -0
- package/dist/testing/index.mjs +2 -0
- package/dist/testing/index.mjs.map +1 -0
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -1
- package/dist/{types-DGZ892my.mjs → types-BI_KmpTc.mjs} +2 -2
- package/dist/types-BI_KmpTc.mjs.map +1 -0
- package/dist/upload/index.d.cts +5 -5
- package/dist/upload/index.d.mts +5 -5
- package/dist/upload/index.mjs +1 -1
- package/dist/{upload-DJTptYqV.mjs → upload-Yj5lrtZo.mjs} +2 -2
- package/dist/{upload-DJTptYqV.mjs.map → upload-Yj5lrtZo.mjs.map} +1 -1
- package/dist/{uploadista-error-9yLWP7TC.d.cts → uploadista-error-BQLhNZcY.d.cts} +1 -1
- package/dist/{uploadista-error-9yLWP7TC.d.cts.map → uploadista-error-BQLhNZcY.d.cts.map} +1 -1
- package/dist/{uploadista-error-nZ_q-EZy.mjs → uploadista-error-Buscq-FR.mjs} +1 -1
- package/dist/{uploadista-error-nZ_q-EZy.mjs.map → uploadista-error-Buscq-FR.mjs.map} +1 -1
- package/dist/{uploadista-error-CBkvsyZ3.d.mts → uploadista-error-DUWw6OqS.d.mts} +1 -1
- package/dist/{uploadista-error-CBkvsyZ3.d.mts.map → uploadista-error-DUWw6OqS.d.mts.map} +1 -1
- package/dist/utils/index.d.cts +2 -2
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-BicUw_lt.mjs → utils-BWiu6lqv.mjs} +2 -2
- package/dist/{utils-BicUw_lt.mjs.map → utils-BWiu6lqv.mjs.map} +1 -1
- package/package.json +14 -6
- package/src/flow/node.ts +4 -4
- package/src/flow/nodes/transform-node.ts +23 -2
- package/src/flow/plugins/credential-provider.ts +1 -1
- package/src/flow/plugins/image-ai-plugin.ts +1 -1
- package/src/flow/plugins/image-plugin.ts +1 -1
- package/src/flow/plugins/video-plugin.ts +1 -1
- package/src/flow/plugins/zip-plugin.ts +1 -1
- package/src/flow/types/type-utils.ts +14 -3
- package/src/testing/index.ts +14 -0
- package/src/testing/mock-image-ai-plugin.ts +33 -0
- package/src/testing/mock-image-plugin.ts +56 -0
- package/src/testing/mock-upload-server.ts +176 -0
- package/src/testing/mock-video-plugin.ts +94 -0
- package/src/testing/mock-zip-plugin.ts +41 -0
- package/src/types/data-store.ts +1 -1
- package/{src/errors/__tests__ → tests/errors}/uploadista-error.test.ts +23 -19
- package/{src → tests}/flow/edge.test.ts +1 -1
- package/tests/flow/flow.test.ts +853 -0
- package/tests/flow/node.test.ts +757 -0
- package/{src → tests}/streams/stream-limiter.test.ts +2 -2
- package/tests/types/typed-event-emitter.test.ts +282 -0
- package/{src → tests}/utils/debounce.test.ts +1 -1
- package/{src → tests}/utils/once.test.ts +1 -1
- package/tests/utils/test-layers.ts +183 -0
- package/{src → tests}/utils/throttle.test.ts +1 -1
- package/tsdown.config.ts +1 -0
- package/type-tests/flow.test-d.ts +93 -0
- package/type-tests/type-utils.test-d.ts +104 -51
- package/vitest.config.ts +19 -1
- package/dist/flow-IgE8hj7H.mjs.map +0 -1
- package/dist/index-CrZopnP9.d.cts.map +0 -1
- package/dist/index-DHt7Ht_J.d.mts.map +0 -1
- package/dist/index-DubOIur4.d.cts.map +0 -1
- package/dist/types-DGZ892my.mjs.map +0 -1
- /package/dist/{errors-C0zLx77t.mjs → errors-DEFjN-xn.mjs} +0 -0
- /package/dist/{index-BtBZHVmz.d.cts → index-C-svZlpj.d.mts} +0 -0
- /package/dist/{index-DEHBdV_z.d.mts → index-_wQ5ClJU.d.cts} +0 -0
- /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 "
|
|
3
|
-
import { streamLimiter } from "
|
|
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
|
+
});
|
|
@@ -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
|
+
};
|
package/tsdown.config.ts
CHANGED
|
@@ -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
|
+
);
|