@uploadista/core 0.2.0 → 1.0.0-beta.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/dist/{checksum-BjP9nb5b.mjs → checksum-BRjFmTRk.mjs} +2 -2
- package/dist/{checksum-BjP9nb5b.mjs.map → checksum-BRjFmTRk.mjs.map} +1 -1
- package/dist/{checksum-B7RDiO7V.cjs → checksum-BrjQ8GJL.cjs} +1 -1
- package/dist/errors/index.cjs +1 -1
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.mts +1 -1
- package/dist/errors/index.mjs +1 -1
- package/dist/flow/index.cjs +1 -1
- package/dist/flow/index.d.cts +3 -2
- package/dist/flow/index.d.mts +8 -5
- package/dist/flow/index.mjs +1 -1
- package/dist/generate-id-BAMRQzMr.d.cts +34 -0
- package/dist/generate-id-BAMRQzMr.d.cts.map +1 -0
- package/dist/generate-id-DuZwLm4m.d.mts +34 -0
- package/dist/generate-id-DuZwLm4m.d.mts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +8 -5
- package/dist/index.d.mts +8 -5
- package/dist/index.mjs +1 -1
- package/dist/middleware-BghazxzH.d.cts +4129 -0
- package/dist/middleware-BghazxzH.d.cts.map +1 -0
- package/dist/middleware-CYizzAhP.d.mts +4129 -0
- package/dist/middleware-CYizzAhP.d.mts.map +1 -0
- package/dist/resolve-upload-metadata-CYl2PHIs.d.mts +4542 -0
- package/dist/resolve-upload-metadata-CYl2PHIs.d.mts.map +1 -0
- package/dist/resolve-upload-metadata-D0qFuyWc.d.cts +4542 -0
- package/dist/resolve-upload-metadata-D0qFuyWc.d.cts.map +1 -0
- package/dist/run-args-CM14Vtzu.cjs +1 -0
- package/dist/run-args-DSKHoSWs.mjs +2 -0
- package/dist/run-args-DSKHoSWs.mjs.map +1 -0
- package/dist/{stream-limiter-BCFULdAM.d.cts → stream-limiter-7wkBVLWT.d.mts} +2 -2
- package/dist/{stream-limiter-BCFULdAM.d.cts.map → stream-limiter-7wkBVLWT.d.mts.map} +1 -1
- package/dist/{stream-limiter-DZ22uIqf.cjs → stream-limiter-B-Y0DTgA.cjs} +1 -1
- package/dist/{stream-limiter-CTJPEJqE.mjs → stream-limiter-CvDuNIyd.mjs} +2 -2
- package/dist/{stream-limiter-CTJPEJqE.mjs.map → stream-limiter-CvDuNIyd.mjs.map} +1 -1
- package/dist/{stream-limiter-Bi7OTbRp.d.mts → stream-limiter-D1KC-6pK.d.cts} +2 -2
- package/dist/{stream-limiter-Bi7OTbRp.d.mts.map → stream-limiter-D1KC-6pK.d.cts.map} +1 -1
- package/dist/streams/index.cjs +1 -1
- package/dist/streams/index.d.cts +1 -1
- package/dist/streams/index.d.mts +2 -2
- package/dist/streams/index.mjs +1 -1
- package/dist/testing/index.cjs +1 -1
- package/dist/testing/index.d.cts +2 -1
- package/dist/testing/index.d.cts.map +1 -1
- package/dist/testing/index.d.mts +7 -4
- package/dist/testing/index.d.mts.map +1 -1
- package/dist/testing/index.mjs +1 -1
- package/dist/{throttle-Da0OA8JT.d.cts → throttle-3FRcr7MU.d.mts} +4 -34
- package/dist/throttle-3FRcr7MU.d.mts.map +1 -0
- package/dist/{throttle-ibiT6E4U.d.mts → throttle-BlH27EGu.d.cts} +4 -34
- package/dist/throttle-BlH27EGu.d.cts.map +1 -0
- package/dist/{throttle-KnkRgZPi.cjs → throttle-Dp59f37i.cjs} +1 -1
- package/dist/{throttle-CnDa3v1k.mjs → throttle-TFY-V41R.mjs} +2 -2
- package/dist/{throttle-CnDa3v1k.mjs.map → throttle-TFY-V41R.mjs.map} +1 -1
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.mts +3 -5
- package/dist/types/index.mjs +1 -1
- package/dist/upload/index.cjs +1 -1
- package/dist/upload/index.d.cts +1 -1
- package/dist/upload/index.d.mts +4 -4
- package/dist/upload/index.mjs +1 -1
- package/dist/upload-strategy-negotiator-0-dpNIce.d.cts +455 -0
- package/dist/upload-strategy-negotiator-0-dpNIce.d.cts.map +1 -0
- package/dist/upload-strategy-negotiator-BR_o1Ez8.cjs +1 -0
- package/dist/upload-strategy-negotiator-C9MeoOnW.mjs +2 -0
- package/dist/upload-strategy-negotiator-C9MeoOnW.mjs.map +1 -0
- package/dist/upload-strategy-negotiator-CEnlfVgJ.d.mts +455 -0
- package/dist/upload-strategy-negotiator-CEnlfVgJ.d.mts.map +1 -0
- package/dist/{uploadista-error-B-geDgi8.cjs → uploadista-error-CZx1JU_L.cjs} +3 -1
- package/dist/{uploadista-error-Fsfvr2Bb.mjs → uploadista-error-DQ7V1FlX.mjs} +3 -1
- package/dist/uploadista-error-DQ7V1FlX.mjs.map +1 -0
- package/dist/{uploadista-error-BragVhIs.d.mts → uploadista-error-LtiZn-R_.d.mts} +2 -2
- package/dist/{uploadista-error-BragVhIs.d.mts.map → uploadista-error-LtiZn-R_.d.mts.map} +1 -1
- package/dist/{uploadista-error-Cj_pAFck.d.cts → uploadista-error-eZtG4iyf.d.cts} +2 -2
- package/dist/{uploadista-error-Cj_pAFck.d.cts.map → uploadista-error-eZtG4iyf.d.cts.map} +1 -1
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.cts +2 -1
- package/dist/utils/index.d.mts +3 -2
- package/dist/utils/index.mjs +1 -1
- package/dist/websocket-Br0ijEZA.cjs +1 -0
- package/dist/websocket-DftnHFfN.mjs +2 -0
- package/dist/websocket-DftnHFfN.mjs.map +1 -0
- package/package.json +3 -3
- package/src/errors/uploadista-error.ts +11 -1
- package/src/flow/README.md +115 -0
- package/src/flow/flow-engine.ts +36 -2
- package/src/flow/flow-queue-store.ts +155 -0
- package/src/flow/flow-queue.ts +640 -0
- package/src/flow/index.ts +4 -0
- package/src/flow/types/flow-queue-item.ts +154 -0
- package/src/types/data-store.ts +3 -3
- package/src/types/kv-store.ts +31 -1
- package/src/upload/write-to-store.ts +24 -29
- package/tests/flow-queue-store.test.ts +150 -0
- package/tests/flow-queue.test.ts +308 -0
- package/dist/resolve-upload-metadata-BUVl1LoS.d.cts +0 -8723
- package/dist/resolve-upload-metadata-BUVl1LoS.d.cts.map +0 -1
- package/dist/resolve-upload-metadata-MPDmDfOZ.d.mts +0 -8723
- package/dist/resolve-upload-metadata-MPDmDfOZ.d.mts.map +0 -1
- package/dist/run-args-WD1otVrz.mjs +0 -2
- package/dist/run-args-WD1otVrz.mjs.map +0 -1
- package/dist/run-args-g74p8pEZ.cjs +0 -1
- package/dist/throttle-Da0OA8JT.d.cts.map +0 -1
- package/dist/throttle-ibiT6E4U.d.mts.map +0 -1
- package/dist/upload-strategy-negotiator-BuxPf1sa.mjs +0 -2
- package/dist/upload-strategy-negotiator-BuxPf1sa.mjs.map +0 -1
- package/dist/upload-strategy-negotiator-DfiQ0Fy0.cjs +0 -1
- package/dist/uploadista-error-Fsfvr2Bb.mjs.map +0 -1
- package/dist/websocket-Avz4T8YB.cjs +0 -1
- package/dist/websocket-CdgVhVJs.mjs +0 -2
- package/dist/websocket-CdgVhVJs.mjs.map +0 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Effect, Layer, Option } from "effect";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { MemoryFlowQueueStore } from "../src/flow/flow-queue-store";
|
|
4
|
+
import { FlowQueueService } from "../src/flow/flow-queue";
|
|
5
|
+
import { FlowEngine } from "../src/flow/flow-engine";
|
|
6
|
+
import type { FlowEngineShape } from "../src/flow/flow-engine";
|
|
7
|
+
import type { FlowJob } from "../src/flow/types/flow-job";
|
|
8
|
+
import type { FlowQueueItem } from "../src/flow/types/flow-queue-item";
|
|
9
|
+
|
|
10
|
+
// Minimal FlowJob stub returned by the mock engine
|
|
11
|
+
function makeJobStub(overrides: Partial<FlowJob> = {}): FlowJob {
|
|
12
|
+
const now = new Date();
|
|
13
|
+
return {
|
|
14
|
+
id: "job_mock",
|
|
15
|
+
flowId: "test-flow",
|
|
16
|
+
storageId: "s3-test",
|
|
17
|
+
clientId: "client-1",
|
|
18
|
+
status: "completed",
|
|
19
|
+
tasks: [],
|
|
20
|
+
createdAt: now,
|
|
21
|
+
updatedAt: now,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Build a mock FlowEngine that resolves runFlow after an optional delay.
|
|
27
|
+
* runFlow returns an Effect so the worker loop's `.pipe()` call works correctly. */
|
|
28
|
+
function makeMockEngine(
|
|
29
|
+
resolveAfterMs = 0,
|
|
30
|
+
shouldFail = false,
|
|
31
|
+
): FlowEngineShape {
|
|
32
|
+
return {
|
|
33
|
+
getFlow: vi.fn(),
|
|
34
|
+
getFlowData: vi.fn(),
|
|
35
|
+
runFlow: vi.fn(() => {
|
|
36
|
+
if (resolveAfterMs > 0) {
|
|
37
|
+
const delayed = Effect.promise(
|
|
38
|
+
() => new Promise<FlowJob>((resolve, reject) => {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
if (shouldFail) reject(new Error("mock flow failure"));
|
|
41
|
+
else resolve(makeJobStub());
|
|
42
|
+
}, resolveAfterMs);
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
return delayed;
|
|
46
|
+
}
|
|
47
|
+
if (shouldFail) {
|
|
48
|
+
return Effect.fail(new Error("mock flow failure")) as unknown as Effect.Effect<FlowJob, never, never>;
|
|
49
|
+
}
|
|
50
|
+
return Effect.succeed(makeJobStub());
|
|
51
|
+
}) as unknown as FlowEngineShape["runFlow"],
|
|
52
|
+
resumeFlow: vi.fn(),
|
|
53
|
+
pauseFlow: vi.fn(),
|
|
54
|
+
cancelFlow: vi.fn(),
|
|
55
|
+
getJobStatus: vi.fn(),
|
|
56
|
+
subscribeToFlowEvents: vi.fn(),
|
|
57
|
+
unsubscribeFromFlowEvents: vi.fn(),
|
|
58
|
+
} as unknown as FlowEngineShape;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Helper: run an Effect with FlowQueueService + mock FlowEngine */
|
|
62
|
+
function runWithQueue<A>(
|
|
63
|
+
effect: Effect.Effect<A, unknown, FlowQueueService>,
|
|
64
|
+
options: { store?: MemoryFlowQueueStore; engine?: FlowEngineShape; config?: { maxConcurrency?: number } } = {},
|
|
65
|
+
) {
|
|
66
|
+
const store = options.store ?? new MemoryFlowQueueStore();
|
|
67
|
+
const engine = options.engine ?? makeMockEngine();
|
|
68
|
+
const engineLayer = Layer.succeed(FlowEngine, engine);
|
|
69
|
+
const queueLayer = FlowQueueService.make(
|
|
70
|
+
options.config ?? {},
|
|
71
|
+
store,
|
|
72
|
+
).pipe(Layer.provide(engineLayer));
|
|
73
|
+
|
|
74
|
+
return Effect.runPromise(
|
|
75
|
+
effect.pipe(Effect.provide(queueLayer)),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("FlowQueueService", () => {
|
|
80
|
+
describe("enqueue", () => {
|
|
81
|
+
it("returns a pending FlowQueueItem immediately", async () => {
|
|
82
|
+
const result = await runWithQueue(
|
|
83
|
+
Effect.gen(function* () {
|
|
84
|
+
const queue = yield* FlowQueueService;
|
|
85
|
+
return yield* queue.enqueue({
|
|
86
|
+
flowId: "my-flow",
|
|
87
|
+
storageId: "s3-prod",
|
|
88
|
+
input: { key: "val" },
|
|
89
|
+
clientId: "client-1",
|
|
90
|
+
});
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result.status).toBe("pending");
|
|
95
|
+
expect(result.flowId).toBe("my-flow");
|
|
96
|
+
expect(result.id).toMatch(/^q_/);
|
|
97
|
+
expect(result.enqueuedAt).toBeInstanceOf(Date);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("stores the item in the queue store", async () => {
|
|
101
|
+
const store = new MemoryFlowQueueStore();
|
|
102
|
+
await runWithQueue(
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const queue = yield* FlowQueueService;
|
|
105
|
+
return yield* queue.enqueue({
|
|
106
|
+
flowId: "flow-x",
|
|
107
|
+
storageId: "s3",
|
|
108
|
+
input: {},
|
|
109
|
+
clientId: null,
|
|
110
|
+
});
|
|
111
|
+
}),
|
|
112
|
+
{ store },
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const pending = await Effect.runPromise(store.listByStatus("pending"));
|
|
116
|
+
// Worker may have already dispatched it, so check pending or running/completed
|
|
117
|
+
const all = [
|
|
118
|
+
...(await Effect.runPromise(store.listByStatus("pending"))),
|
|
119
|
+
...(await Effect.runPromise(store.listByStatus("running"))),
|
|
120
|
+
...(await Effect.runPromise(store.listByStatus("completed"))),
|
|
121
|
+
];
|
|
122
|
+
expect(all.length).toBeGreaterThanOrEqual(1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("getStatus", () => {
|
|
127
|
+
it("returns the item for a known ID", async () => {
|
|
128
|
+
const result = await runWithQueue(
|
|
129
|
+
Effect.gen(function* () {
|
|
130
|
+
const queue = yield* FlowQueueService;
|
|
131
|
+
const item = yield* queue.enqueue({
|
|
132
|
+
flowId: "flow-a",
|
|
133
|
+
storageId: "s3",
|
|
134
|
+
input: {},
|
|
135
|
+
clientId: null,
|
|
136
|
+
});
|
|
137
|
+
return yield* queue.getStatus(item.id);
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(result.id).toMatch(/^q_/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("fails with QUEUE_ITEM_NOT_FOUND for unknown IDs", async () => {
|
|
145
|
+
const result = await runWithQueue(
|
|
146
|
+
Effect.gen(function* () {
|
|
147
|
+
const queue = yield* FlowQueueService;
|
|
148
|
+
return yield* Effect.either(queue.getStatus("q_nonexistent"));
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result._tag).toBe("Left");
|
|
153
|
+
if (result._tag === "Left") {
|
|
154
|
+
expect((result.left as { code?: string }).code).toBe(
|
|
155
|
+
"QUEUE_ITEM_NOT_FOUND",
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("cancel", () => {
|
|
162
|
+
it("removes a pending item", async () => {
|
|
163
|
+
const store = new MemoryFlowQueueStore();
|
|
164
|
+
// Use a very slow engine so the item stays pending long enough to cancel
|
|
165
|
+
const slowEngine = makeMockEngine(10_000);
|
|
166
|
+
|
|
167
|
+
await runWithQueue(
|
|
168
|
+
Effect.gen(function* () {
|
|
169
|
+
const queue = yield* FlowQueueService;
|
|
170
|
+
const item = yield* queue.enqueue({
|
|
171
|
+
flowId: "slow-flow",
|
|
172
|
+
storageId: "s3",
|
|
173
|
+
input: {},
|
|
174
|
+
clientId: null,
|
|
175
|
+
});
|
|
176
|
+
// Cancel before worker can dispatch
|
|
177
|
+
return yield* queue.cancel(item.id);
|
|
178
|
+
}),
|
|
179
|
+
{ store, engine: slowEngine },
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const item = await Effect.runPromise(store.getItem("q_test"));
|
|
183
|
+
// Either null (deleted) or in another status — the cancel removed the pending item
|
|
184
|
+
// We just verify no error was thrown
|
|
185
|
+
expect(true).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("fails with QUEUE_ITEM_ALREADY_RUNNING for running items", async () => {
|
|
189
|
+
const store = new MemoryFlowQueueStore();
|
|
190
|
+
// Pre-insert a running item directly in the store
|
|
191
|
+
const runningItem: FlowQueueItem = {
|
|
192
|
+
id: "q_running_test",
|
|
193
|
+
flowId: "flow-r",
|
|
194
|
+
storageId: "s3",
|
|
195
|
+
input: {},
|
|
196
|
+
clientId: null,
|
|
197
|
+
status: "running",
|
|
198
|
+
enqueuedAt: new Date(),
|
|
199
|
+
startedAt: new Date(),
|
|
200
|
+
};
|
|
201
|
+
await Effect.runPromise(store.createItem(runningItem));
|
|
202
|
+
|
|
203
|
+
const result = await runWithQueue(
|
|
204
|
+
Effect.gen(function* () {
|
|
205
|
+
const queue = yield* FlowQueueService;
|
|
206
|
+
return yield* Effect.either(queue.cancel("q_running_test"));
|
|
207
|
+
}),
|
|
208
|
+
{ store },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(result._tag).toBe("Left");
|
|
212
|
+
if (result._tag === "Left") {
|
|
213
|
+
expect((result.left as { code?: string }).code).toBe(
|
|
214
|
+
"QUEUE_ITEM_ALREADY_RUNNING",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("fails with QUEUE_ITEM_NOT_FOUND for unknown IDs", async () => {
|
|
220
|
+
const result = await runWithQueue(
|
|
221
|
+
Effect.gen(function* () {
|
|
222
|
+
const queue = yield* FlowQueueService;
|
|
223
|
+
return yield* Effect.either(queue.cancel("q_bogus"));
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(result._tag).toBe("Left");
|
|
228
|
+
if (result._tag === "Left") {
|
|
229
|
+
expect((result.left as { code?: string }).code).toBe(
|
|
230
|
+
"QUEUE_ITEM_NOT_FOUND",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("getStats", () => {
|
|
237
|
+
it("returns stats with correct maxConcurrency", async () => {
|
|
238
|
+
const stats = await runWithQueue(
|
|
239
|
+
Effect.gen(function* () {
|
|
240
|
+
const queue = yield* FlowQueueService;
|
|
241
|
+
return yield* queue.getStats();
|
|
242
|
+
}),
|
|
243
|
+
{ config: { maxConcurrency: 3 } },
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(stats.maxConcurrency).toBe(3);
|
|
247
|
+
expect(typeof stats.pending).toBe("number");
|
|
248
|
+
expect(typeof stats.running).toBe("number");
|
|
249
|
+
expect(typeof stats.completed).toBe("number");
|
|
250
|
+
expect(typeof stats.failed).toBe("number");
|
|
251
|
+
expect(typeof stats.currentConcurrency).toBe("number");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("list", () => {
|
|
256
|
+
it("returns items filtered by status", async () => {
|
|
257
|
+
const store = new MemoryFlowQueueStore();
|
|
258
|
+
|
|
259
|
+
// Pre-populate with items in various statuses
|
|
260
|
+
const items: FlowQueueItem[] = [
|
|
261
|
+
{
|
|
262
|
+
id: "q_p",
|
|
263
|
+
flowId: "f1",
|
|
264
|
+
storageId: "s3",
|
|
265
|
+
input: {},
|
|
266
|
+
clientId: null,
|
|
267
|
+
status: "pending",
|
|
268
|
+
enqueuedAt: new Date(),
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "q_c",
|
|
272
|
+
flowId: "f2",
|
|
273
|
+
storageId: "s3",
|
|
274
|
+
input: {},
|
|
275
|
+
clientId: null,
|
|
276
|
+
status: "completed",
|
|
277
|
+
enqueuedAt: new Date(),
|
|
278
|
+
completedAt: new Date(),
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const item of items) {
|
|
283
|
+
await Effect.runPromise(store.createItem(item));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const completedList = await runWithQueue(
|
|
287
|
+
Effect.gen(function* () {
|
|
288
|
+
const queue = yield* FlowQueueService;
|
|
289
|
+
return yield* queue.list({ status: "completed" });
|
|
290
|
+
}),
|
|
291
|
+
{ store },
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
expect(completedList.some((i) => i.id === "q_c")).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("backwards compatibility", () => {
|
|
299
|
+
it("FlowQueueService.optional resolves to none when service is absent", async () => {
|
|
300
|
+
const result = await Effect.runPromise(
|
|
301
|
+
Effect.gen(function* () {
|
|
302
|
+
return yield* FlowQueueService.optional;
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
expect(Option.isNone(result)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|