@uploadista/adapters-express 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +456 -0
- package/USAGE.md +164 -0
- package/dist/adapter-layer.d.ts +22 -0
- package/dist/adapter-layer.d.ts.map +1 -0
- package/dist/adapter-layer.js +3 -0
- package/dist/error-types.d.ts +24 -0
- package/dist/error-types.d.ts.map +1 -0
- package/dist/error-types.js +65 -0
- package/dist/flow-adapter.d.ts +19 -0
- package/dist/flow-adapter.d.ts.map +1 -0
- package/dist/flow-adapter.js +80 -0
- package/dist/flow-http-handlers.d.ts +9 -0
- package/dist/flow-http-handlers.d.ts.map +1 -0
- package/dist/flow-http-handlers.js +133 -0
- package/dist/http-handlers.d.ts +7 -0
- package/dist/http-handlers.d.ts.map +1 -0
- package/dist/http-handlers.js +78 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/upload-http-handlers.d.ts +9 -0
- package/dist/upload-http-handlers.d.ts.map +1 -0
- package/dist/upload-http-handlers.js +113 -0
- package/dist/uploadista-adapter-layer.d.ts +24 -0
- package/dist/uploadista-adapter-layer.d.ts.map +1 -0
- package/dist/uploadista-adapter-layer.js +4 -0
- package/dist/uploadista-adapter.d.ts +78 -0
- package/dist/uploadista-adapter.d.ts.map +1 -0
- package/dist/uploadista-adapter.js +297 -0
- package/dist/uploadista-websocket-handler.d.ts +9 -0
- package/dist/uploadista-websocket-handler.d.ts.map +1 -0
- package/dist/uploadista-websocket-handler.js +132 -0
- package/dist/websocket-handler.d.ts +8 -0
- package/dist/websocket-handler.d.ts.map +1 -0
- package/dist/websocket-handler.js +82 -0
- package/package.json +40 -0
- package/src/error-types.ts +103 -0
- package/src/flow-http-handlers.ts +184 -0
- package/src/index.ts +14 -0
- package/src/upload-http-handlers.ts +186 -0
- package/src/uploadista-adapter-layer.ts +32 -0
- package/src/uploadista-adapter.ts +626 -0
- package/src/uploadista-websocket-handler.ts +209 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type {
|
|
3
|
+
EventBroadcasterService,
|
|
4
|
+
UploadFileDataStores,
|
|
5
|
+
UploadistaError,
|
|
6
|
+
} from "@uploadista/core";
|
|
7
|
+
import { type Flow, FlowProvider, FlowServer } from "@uploadista/core/flow";
|
|
8
|
+
import {
|
|
9
|
+
type BaseEventEmitterService,
|
|
10
|
+
type BaseKvStoreService,
|
|
11
|
+
createDataStoreLayer,
|
|
12
|
+
type DataStoreConfig,
|
|
13
|
+
type UploadFileDataStore,
|
|
14
|
+
type UploadFileKVStore,
|
|
15
|
+
} from "@uploadista/core/types";
|
|
16
|
+
import { UploadServer } from "@uploadista/core/upload";
|
|
17
|
+
import { type GenerateId, GenerateIdLive } from "@uploadista/core/utils";
|
|
18
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
19
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
20
|
+
import {
|
|
21
|
+
type MetricsService,
|
|
22
|
+
NodeSdkLive,
|
|
23
|
+
NoOpMetricsServiceLive,
|
|
24
|
+
} from "@uploadista/observability";
|
|
25
|
+
import {
|
|
26
|
+
type AuthCacheConfig,
|
|
27
|
+
AuthCacheServiceLive,
|
|
28
|
+
type AuthContext,
|
|
29
|
+
AuthContextServiceLive,
|
|
30
|
+
type AuthResult,
|
|
31
|
+
createFlowServerLayer,
|
|
32
|
+
createUploadServerLayer,
|
|
33
|
+
type FlowRequirementsOf,
|
|
34
|
+
} from "@uploadista/server";
|
|
35
|
+
import { Effect, Layer } from "effect";
|
|
36
|
+
import type { Request, Response } from "express";
|
|
37
|
+
import type { z } from "zod";
|
|
38
|
+
import {
|
|
39
|
+
handleContinueFlow,
|
|
40
|
+
handleFlowGet,
|
|
41
|
+
handleFlowPost,
|
|
42
|
+
handleJobStatus,
|
|
43
|
+
} from "./flow-http-handlers";
|
|
44
|
+
import {
|
|
45
|
+
handleUploadGet,
|
|
46
|
+
handleUploadPatch,
|
|
47
|
+
handleUploadPost,
|
|
48
|
+
} from "./upload-http-handlers";
|
|
49
|
+
import {
|
|
50
|
+
ExpressUploadistaAdapterService,
|
|
51
|
+
type ExpressUploadistaAdapterServiceShape,
|
|
52
|
+
type WebSocketConnection,
|
|
53
|
+
type WebSocketHandlers,
|
|
54
|
+
} from "./uploadista-adapter-layer";
|
|
55
|
+
import { createUploadistaWebSocketHandler } from "./uploadista-websocket-handler";
|
|
56
|
+
|
|
57
|
+
export type ExpressUploadistaAdapterOptions<
|
|
58
|
+
TFlows extends (
|
|
59
|
+
flowId: string,
|
|
60
|
+
clientId: string | null,
|
|
61
|
+
) => Effect.Effect<
|
|
62
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>,
|
|
63
|
+
UploadistaError,
|
|
64
|
+
unknown
|
|
65
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic type constraint allows any flow function type
|
|
66
|
+
> = any,
|
|
67
|
+
// biome-ignore lint/suspicious/noExplicitAny: Permissive constraint allows plugin tuples, validation via PluginAssertion
|
|
68
|
+
TPlugins extends readonly Layer.Layer<any, never, never>[] = Layer.Layer<
|
|
69
|
+
any,
|
|
70
|
+
never,
|
|
71
|
+
never
|
|
72
|
+
>[],
|
|
73
|
+
> = {
|
|
74
|
+
// Flow configuration
|
|
75
|
+
flows: TFlows;
|
|
76
|
+
plugins?: TPlugins;
|
|
77
|
+
|
|
78
|
+
dataStore: DataStoreConfig;
|
|
79
|
+
bufferedDataStore?: Layer.Layer<
|
|
80
|
+
UploadFileDataStore,
|
|
81
|
+
never,
|
|
82
|
+
UploadFileKVStore
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
// Shared configuration
|
|
86
|
+
baseUrl?: string;
|
|
87
|
+
kvStore: Layer.Layer<BaseKvStoreService>;
|
|
88
|
+
eventEmitter?: Layer.Layer<BaseEventEmitterService>;
|
|
89
|
+
eventBroadcaster?: Layer.Layer<EventBroadcasterService>;
|
|
90
|
+
generateId?: Layer.Layer<GenerateId>;
|
|
91
|
+
withTracing?: boolean;
|
|
92
|
+
|
|
93
|
+
// Authentication
|
|
94
|
+
authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>;
|
|
95
|
+
authCacheConfig?: AuthCacheConfig;
|
|
96
|
+
|
|
97
|
+
// Metrics
|
|
98
|
+
metricsLayer?: Layer.Layer<MetricsService, never, never>;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type InternalExpressUploadistaAdapterOptions<
|
|
102
|
+
TRequirements = never,
|
|
103
|
+
TPlugins extends readonly Layer.Layer<TRequirements, never, never>[] = [],
|
|
104
|
+
> = {
|
|
105
|
+
// Flow configuration
|
|
106
|
+
flowProvider: Layer.Layer<FlowProvider>;
|
|
107
|
+
plugins?: TPlugins;
|
|
108
|
+
|
|
109
|
+
// Upload configuration
|
|
110
|
+
dataStore: Layer.Layer<UploadFileDataStores, never, UploadFileKVStore>;
|
|
111
|
+
bufferedDataStore?: Layer.Layer<
|
|
112
|
+
UploadFileDataStore,
|
|
113
|
+
never,
|
|
114
|
+
UploadFileKVStore
|
|
115
|
+
>;
|
|
116
|
+
// Shared configuration
|
|
117
|
+
baseUrl: string;
|
|
118
|
+
kvStore: Layer.Layer<BaseKvStoreService>;
|
|
119
|
+
eventEmitter: Layer.Layer<BaseEventEmitterService>;
|
|
120
|
+
generateId?: Layer.Layer<GenerateId>;
|
|
121
|
+
withTracing?: boolean;
|
|
122
|
+
|
|
123
|
+
// Authentication
|
|
124
|
+
authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>;
|
|
125
|
+
authCacheConfig?: AuthCacheConfig;
|
|
126
|
+
|
|
127
|
+
// Metrics
|
|
128
|
+
metricsLayer?: Layer.Layer<MetricsService, never, never>;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type ExpressUploadistaAdapter = {
|
|
132
|
+
baseUrl: string;
|
|
133
|
+
handler: (
|
|
134
|
+
req: Request,
|
|
135
|
+
res: Response,
|
|
136
|
+
next?: (error?: Error) => void,
|
|
137
|
+
) => void;
|
|
138
|
+
websocketHandler: (
|
|
139
|
+
req: IncomingMessage,
|
|
140
|
+
connection: WebSocketConnection,
|
|
141
|
+
) => WebSocketHandlers;
|
|
142
|
+
websocketConnectionHandler: (ws: WebSocket, req: IncomingMessage) => void;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// WebSocket type from ws package
|
|
146
|
+
interface WebSocket {
|
|
147
|
+
readyState: number;
|
|
148
|
+
OPEN: number;
|
|
149
|
+
send: (data: string) => void;
|
|
150
|
+
close: (code?: number, reason?: string) => void;
|
|
151
|
+
on: (event: string, handler: (...args: any[]) => void) => void;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Effect-native API
|
|
155
|
+
export type ExpressUploadistaServer = {
|
|
156
|
+
handler: (req: Request, res: Response) => Effect.Effect<void, never, never>;
|
|
157
|
+
uploadServer: Layer.Layer<UploadServer>;
|
|
158
|
+
flowServer: Layer.Layer<FlowServer>;
|
|
159
|
+
websocketHandler: (
|
|
160
|
+
req: IncomingMessage,
|
|
161
|
+
connection: WebSocketConnection,
|
|
162
|
+
) => WebSocketHandlers;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Effect-based service factory for creating the unified adapter layer
|
|
166
|
+
const createExpressUploadistaAdapterServiceLayer = (
|
|
167
|
+
baseUrl: string,
|
|
168
|
+
authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>,
|
|
169
|
+
authCacheConfig?: AuthCacheConfig,
|
|
170
|
+
metricsLayer?: Layer.Layer<MetricsService, never, never>,
|
|
171
|
+
) =>
|
|
172
|
+
Layer.effect(
|
|
173
|
+
ExpressUploadistaAdapterService,
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const uploadServer = yield* UploadServer;
|
|
176
|
+
const flowServer = yield* FlowServer;
|
|
177
|
+
|
|
178
|
+
// Create auth cache layer (always present, even if auth is not enabled)
|
|
179
|
+
const authCacheLayer = AuthCacheServiceLive(authCacheConfig);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
handler: (req: Request, res: Response) =>
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
// Call auth middleware if configured and create auth context layer
|
|
185
|
+
let authContext: AuthContext | null = null;
|
|
186
|
+
if (authMiddleware) {
|
|
187
|
+
// Run auth middleware with timeout protection (5 seconds default)
|
|
188
|
+
const authMiddlewareWithTimeout = Effect.tryPromise({
|
|
189
|
+
try: () => authMiddleware(req, res),
|
|
190
|
+
catch: (error) => {
|
|
191
|
+
console.error("Auth middleware error:", error);
|
|
192
|
+
return { _tag: "AuthError" as const, error };
|
|
193
|
+
},
|
|
194
|
+
}).pipe(
|
|
195
|
+
Effect.timeout("5 seconds"),
|
|
196
|
+
Effect.catchAll((error) => {
|
|
197
|
+
// Check if timeout occurred
|
|
198
|
+
if (error && typeof error === "object" && "_tag" in error) {
|
|
199
|
+
if (error._tag === "TimeoutException") {
|
|
200
|
+
console.error(
|
|
201
|
+
"Auth middleware timeout exceeded (5 seconds)",
|
|
202
|
+
);
|
|
203
|
+
return Effect.succeed({
|
|
204
|
+
_tag: "TimeoutError" as const,
|
|
205
|
+
} as const);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return Effect.succeed(null);
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const authResult:
|
|
213
|
+
| AuthContext
|
|
214
|
+
| null
|
|
215
|
+
| { _tag: "TimeoutError" }
|
|
216
|
+
| { _tag: "AuthError"; error: unknown } =
|
|
217
|
+
yield* authMiddlewareWithTimeout;
|
|
218
|
+
|
|
219
|
+
// If auth middleware timed out, return 503 Service Unavailable
|
|
220
|
+
if (
|
|
221
|
+
authResult &&
|
|
222
|
+
typeof authResult === "object" &&
|
|
223
|
+
"_tag" in authResult &&
|
|
224
|
+
authResult._tag === "TimeoutError"
|
|
225
|
+
) {
|
|
226
|
+
res.status(503).json({
|
|
227
|
+
error: "Authentication service unavailable",
|
|
228
|
+
message:
|
|
229
|
+
"Authentication took too long to respond. Please try again.",
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// If auth middleware returned null, authentication failed
|
|
235
|
+
if (authResult === null) {
|
|
236
|
+
res.status(401).json({
|
|
237
|
+
error: "Unauthorized",
|
|
238
|
+
message: "Invalid credentials",
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for error marker (shouldn't happen after catchAll, but for type safety)
|
|
244
|
+
if (
|
|
245
|
+
authResult &&
|
|
246
|
+
typeof authResult === "object" &&
|
|
247
|
+
"_tag" in authResult &&
|
|
248
|
+
authResult._tag === "AuthError"
|
|
249
|
+
) {
|
|
250
|
+
res.status(500).json({
|
|
251
|
+
error: "Internal Server Error",
|
|
252
|
+
message: "An error occurred during authentication",
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
authContext = authResult;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Create auth context layer for this request
|
|
261
|
+
const authContextLayer = AuthContextServiceLive(authContext);
|
|
262
|
+
|
|
263
|
+
// Combine auth context, auth cache, and metrics layers
|
|
264
|
+
// Always provide a metrics layer (either real or no-op) to satisfy type requirements
|
|
265
|
+
const authLayer = Layer.mergeAll(
|
|
266
|
+
authContextLayer,
|
|
267
|
+
authCacheLayer,
|
|
268
|
+
metricsLayer ?? NoOpMetricsServiceLive,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const url = new URL(req.url, `http://${req.get("host")}`);
|
|
272
|
+
|
|
273
|
+
// Check for uploadista/api/ prefix
|
|
274
|
+
if (!url.pathname.includes(`${baseUrl}/api/`)) {
|
|
275
|
+
res.status(404).json({ error: "Not found" });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Remove the prefix and get the actual route segments
|
|
280
|
+
const routeSegments = url.pathname
|
|
281
|
+
.replace(`${baseUrl}/api/`, "")
|
|
282
|
+
.split("/")
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
|
|
285
|
+
// Parse JSON body for routes that need it
|
|
286
|
+
const needsJsonBody =
|
|
287
|
+
(routeSegments.includes("upload") && req.method === "POST") ||
|
|
288
|
+
(routeSegments.includes("flow") && req.method === "POST") ||
|
|
289
|
+
(routeSegments.includes("continue") &&
|
|
290
|
+
req.get("Content-Type")?.includes("application/json"));
|
|
291
|
+
|
|
292
|
+
if (needsJsonBody && !req.body) {
|
|
293
|
+
// Manually parse JSON body
|
|
294
|
+
yield* Effect.tryPromise({
|
|
295
|
+
try: async () => {
|
|
296
|
+
const chunks: Buffer[] = [];
|
|
297
|
+
for await (const chunk of req) {
|
|
298
|
+
chunks.push(chunk as Buffer);
|
|
299
|
+
}
|
|
300
|
+
const body = Buffer.concat(chunks).toString();
|
|
301
|
+
req.body = JSON.parse(body);
|
|
302
|
+
},
|
|
303
|
+
catch: (error) => error,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Route based on path
|
|
308
|
+
if (routeSegments.includes("upload")) {
|
|
309
|
+
// Upload API routes - these now create jobs behind the scenes
|
|
310
|
+
switch (req.method) {
|
|
311
|
+
case "POST":
|
|
312
|
+
return yield* handleUploadPost(req, res, uploadServer).pipe(
|
|
313
|
+
Effect.provide(authLayer),
|
|
314
|
+
);
|
|
315
|
+
case "GET":
|
|
316
|
+
return yield* handleUploadGet(req, res, uploadServer).pipe(
|
|
317
|
+
Effect.provide(authLayer),
|
|
318
|
+
);
|
|
319
|
+
case "PATCH":
|
|
320
|
+
return yield* handleUploadPatch(req, res, uploadServer).pipe(
|
|
321
|
+
Effect.provide(authLayer),
|
|
322
|
+
);
|
|
323
|
+
default:
|
|
324
|
+
res.status(405).json({ error: "Method not allowed" });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
} else if (routeSegments.includes("flow")) {
|
|
328
|
+
// Flow API routes
|
|
329
|
+
switch (req.method) {
|
|
330
|
+
case "GET":
|
|
331
|
+
return yield* handleFlowGet(req, res, flowServer).pipe(
|
|
332
|
+
Effect.provide(authLayer),
|
|
333
|
+
);
|
|
334
|
+
case "POST":
|
|
335
|
+
return yield* handleFlowPost<never>(
|
|
336
|
+
req,
|
|
337
|
+
res,
|
|
338
|
+
flowServer,
|
|
339
|
+
).pipe(Effect.provide(authLayer));
|
|
340
|
+
default:
|
|
341
|
+
res.status(405).json({ error: "Method not allowed" });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
} else if (routeSegments.includes("jobs")) {
|
|
345
|
+
// Unified job status routes
|
|
346
|
+
if (req.method === "GET" && url.pathname.endsWith("/status")) {
|
|
347
|
+
return yield* handleJobStatus(req, res, flowServer).pipe(
|
|
348
|
+
Effect.provide(authLayer),
|
|
349
|
+
);
|
|
350
|
+
} else if (
|
|
351
|
+
req.method === "PATCH" &&
|
|
352
|
+
routeSegments.includes("continue")
|
|
353
|
+
) {
|
|
354
|
+
return yield* handleContinueFlow<never>(
|
|
355
|
+
req,
|
|
356
|
+
res,
|
|
357
|
+
flowServer,
|
|
358
|
+
).pipe(Effect.provide(authLayer));
|
|
359
|
+
}
|
|
360
|
+
res.status(405).json({ error: "Method not allowed" });
|
|
361
|
+
return;
|
|
362
|
+
} else {
|
|
363
|
+
res.status(404).json({ error: "Not found" });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}).pipe(
|
|
367
|
+
Effect.catchAll(() =>
|
|
368
|
+
Effect.sync(() => {
|
|
369
|
+
res.status(500).json({ error: "Internal server error" });
|
|
370
|
+
}),
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
|
|
374
|
+
websocketHandler: createUploadistaWebSocketHandler(
|
|
375
|
+
baseUrl,
|
|
376
|
+
uploadServer,
|
|
377
|
+
flowServer,
|
|
378
|
+
),
|
|
379
|
+
} satisfies ExpressUploadistaAdapterServiceShape;
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Creates an Effect-native unified Express server - combining upload and flow capabilities
|
|
385
|
+
*/
|
|
386
|
+
export const createExpressUploadistaServer = <TRequirements = UploadServer>({
|
|
387
|
+
baseUrl,
|
|
388
|
+
flowProvider,
|
|
389
|
+
eventEmitter,
|
|
390
|
+
dataStore,
|
|
391
|
+
bufferedDataStore,
|
|
392
|
+
kvStore,
|
|
393
|
+
generateId = GenerateIdLive,
|
|
394
|
+
authMiddleware,
|
|
395
|
+
authCacheConfig,
|
|
396
|
+
metricsLayer,
|
|
397
|
+
}: InternalExpressUploadistaAdapterOptions<TRequirements>): Effect.Effect<ExpressUploadistaServer> => {
|
|
398
|
+
// Set up upload server dependencies using shared utility
|
|
399
|
+
const uploadServerLayer = createUploadServerLayer({
|
|
400
|
+
kvStore,
|
|
401
|
+
eventEmitter,
|
|
402
|
+
dataStore,
|
|
403
|
+
bufferedDataStore,
|
|
404
|
+
generateId,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Set up flow server dependencies using shared utility
|
|
408
|
+
const flowServerLayer = createFlowServerLayer({
|
|
409
|
+
kvStore,
|
|
410
|
+
eventEmitter,
|
|
411
|
+
flowProvider,
|
|
412
|
+
uploadServer: uploadServerLayer,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Set up adapter
|
|
416
|
+
const adapterLayer = Layer.provide(
|
|
417
|
+
createExpressUploadistaAdapterServiceLayer(
|
|
418
|
+
baseUrl,
|
|
419
|
+
authMiddleware,
|
|
420
|
+
authCacheConfig,
|
|
421
|
+
metricsLayer,
|
|
422
|
+
),
|
|
423
|
+
Layer.mergeAll(uploadServerLayer, flowServerLayer),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return Effect.gen(function* () {
|
|
427
|
+
const adapterService = yield* ExpressUploadistaAdapterService;
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
handler: (req: Request, res: Response) =>
|
|
431
|
+
adapterService.handler(req, res),
|
|
432
|
+
websocketHandler: (
|
|
433
|
+
req: IncomingMessage,
|
|
434
|
+
connection: WebSocketConnection,
|
|
435
|
+
) => adapterService.websocketHandler(req, connection),
|
|
436
|
+
uploadServer: uploadServerLayer,
|
|
437
|
+
flowServer: flowServerLayer,
|
|
438
|
+
} satisfies ExpressUploadistaServer;
|
|
439
|
+
}).pipe(Effect.provide(adapterLayer));
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const runProgram = <A, E>(
|
|
443
|
+
effect: Effect.Effect<A, E>,
|
|
444
|
+
withTracing: boolean,
|
|
445
|
+
) => {
|
|
446
|
+
if (withTracing) {
|
|
447
|
+
return Effect.runPromise(effect.pipe(Effect.provide(NodeSdkLive)));
|
|
448
|
+
}
|
|
449
|
+
return Effect.runPromise(effect);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Creates a Promise-based Express adapter for compatibility with existing Express applications
|
|
454
|
+
* This wraps the Effect-native version with Promise conversion and caches the server instance
|
|
455
|
+
*/
|
|
456
|
+
export const createInternalExpressUploadistaAdapter = async <
|
|
457
|
+
TRequirements = UploadServer,
|
|
458
|
+
TPlugins extends readonly Layer.Layer<TRequirements, never, never>[] = [],
|
|
459
|
+
>({
|
|
460
|
+
baseUrl,
|
|
461
|
+
flowProvider,
|
|
462
|
+
plugins,
|
|
463
|
+
eventEmitter,
|
|
464
|
+
dataStore,
|
|
465
|
+
bufferedDataStore,
|
|
466
|
+
kvStore,
|
|
467
|
+
withTracing = false,
|
|
468
|
+
authMiddleware,
|
|
469
|
+
authCacheConfig,
|
|
470
|
+
metricsLayer,
|
|
471
|
+
}: InternalExpressUploadistaAdapterOptions<
|
|
472
|
+
TRequirements,
|
|
473
|
+
TPlugins
|
|
474
|
+
>): Promise<ExpressUploadistaAdapter> => {
|
|
475
|
+
// Create and cache the Effect server instance
|
|
476
|
+
const uploadistaServer = await Effect.runPromise(
|
|
477
|
+
createExpressUploadistaServer<TRequirements>({
|
|
478
|
+
baseUrl,
|
|
479
|
+
flowProvider,
|
|
480
|
+
eventEmitter,
|
|
481
|
+
dataStore,
|
|
482
|
+
bufferedDataStore,
|
|
483
|
+
kvStore,
|
|
484
|
+
authMiddleware,
|
|
485
|
+
authCacheConfig,
|
|
486
|
+
metricsLayer,
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// Merge all plugin layers so we can provide them when running handlers
|
|
491
|
+
const pluginLayers = Layer.mergeAll(
|
|
492
|
+
uploadistaServer.uploadServer,
|
|
493
|
+
...(plugins ?? []),
|
|
494
|
+
) as Layer.Layer<UploadServer | TRequirements, never, never>;
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
baseUrl,
|
|
498
|
+
handler: (req: Request, res: Response, next) => {
|
|
499
|
+
runProgram(
|
|
500
|
+
uploadistaServer.handler(req, res).pipe(Effect.provide(pluginLayers)),
|
|
501
|
+
withTracing,
|
|
502
|
+
).catch((error) => {
|
|
503
|
+
console.error("Express adapter error:", error);
|
|
504
|
+
if (next) next(error);
|
|
505
|
+
});
|
|
506
|
+
},
|
|
507
|
+
websocketHandler: (req: IncomingMessage, connection: WebSocketConnection) =>
|
|
508
|
+
uploadistaServer.websocketHandler(req, connection),
|
|
509
|
+
websocketConnectionHandler: (ws: WebSocket, req: IncomingMessage) => {
|
|
510
|
+
// Filter to only handle uploadista WebSocket paths
|
|
511
|
+
if (!req.url?.startsWith(`/${baseUrl}/ws/`)) {
|
|
512
|
+
ws.close(1008, "Invalid WebSocket path");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
console.log(`📡 WebSocket connected: ${req.url}`);
|
|
517
|
+
|
|
518
|
+
const connection: WebSocketConnection = {
|
|
519
|
+
id: `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
520
|
+
send: (data: string) => {
|
|
521
|
+
if (ws.readyState === ws.OPEN) {
|
|
522
|
+
ws.send(data);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
close: (code?: number, reason?: string) => ws.close(code, reason),
|
|
526
|
+
readyState: ws.readyState,
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// Initialize WebSocket handler with Uploadista and get event handlers
|
|
530
|
+
const handlers = uploadistaServer.websocketHandler(req, connection);
|
|
531
|
+
|
|
532
|
+
// Attach event handlers
|
|
533
|
+
ws.on("message", (message: Buffer) => {
|
|
534
|
+
handlers.onMessage(message.toString());
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
ws.on("close", () => {
|
|
538
|
+
handlers.onClose();
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
ws.on("error", (error: Error) => {
|
|
542
|
+
handlers.onError(error);
|
|
543
|
+
});
|
|
544
|
+
},
|
|
545
|
+
} satisfies ExpressUploadistaAdapter;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Creates a Promise-based Uploadista Express adapter for compatibility
|
|
550
|
+
*
|
|
551
|
+
* Note: Ensure that the plugins array provides all services required by your flows.
|
|
552
|
+
* Missing plugin services will result in runtime errors during flow execution.
|
|
553
|
+
*/
|
|
554
|
+
export const createExpressUploadistaAdapter = async <
|
|
555
|
+
TFlows extends (
|
|
556
|
+
flowId: string,
|
|
557
|
+
clientId: string | null,
|
|
558
|
+
) => Effect.Effect<
|
|
559
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>,
|
|
560
|
+
UploadistaError,
|
|
561
|
+
unknown
|
|
562
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic type constraint allows any flow function type
|
|
563
|
+
> = any,
|
|
564
|
+
// biome-ignore lint/suspicious/noExplicitAny: Permissive constraint allows plugin tuples, validation done at runtime
|
|
565
|
+
TPlugins extends readonly Layer.Layer<any, never, never>[] = Layer.Layer<
|
|
566
|
+
any,
|
|
567
|
+
never,
|
|
568
|
+
never
|
|
569
|
+
>[],
|
|
570
|
+
>({
|
|
571
|
+
baseUrl = "uploadista",
|
|
572
|
+
flows,
|
|
573
|
+
// Default to an empty plugin list while preserving the generic type
|
|
574
|
+
plugins = [] as unknown as TPlugins,
|
|
575
|
+
eventEmitter,
|
|
576
|
+
eventBroadcaster = memoryEventBroadcaster,
|
|
577
|
+
dataStore,
|
|
578
|
+
bufferedDataStore,
|
|
579
|
+
kvStore,
|
|
580
|
+
generateId = GenerateIdLive,
|
|
581
|
+
authMiddleware,
|
|
582
|
+
authCacheConfig,
|
|
583
|
+
metricsLayer,
|
|
584
|
+
}: ExpressUploadistaAdapterOptions<
|
|
585
|
+
TFlows,
|
|
586
|
+
TPlugins
|
|
587
|
+
>): Promise<ExpressUploadistaAdapter> => {
|
|
588
|
+
type FlowReq = FlowRequirementsOf<TFlows>;
|
|
589
|
+
// Create a simplified flow provider that uses the flows function directly
|
|
590
|
+
const createFlowProvider = Effect.succeed({
|
|
591
|
+
getFlow: (flowId: string, clientId: string | null) => {
|
|
592
|
+
// The flows function returns an Effect with TRequirements context,
|
|
593
|
+
// but the FlowProvider interface expects no context.
|
|
594
|
+
// We cast this to match the interface - the requirements will be provided
|
|
595
|
+
// at the layer level when the flow adapter is created.
|
|
596
|
+
return flows(flowId, clientId) as Effect.Effect<
|
|
597
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, FlowReq>,
|
|
598
|
+
UploadistaError
|
|
599
|
+
>;
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Create the flow provider layer that provides the requirements
|
|
604
|
+
const flowProvider = Layer.effect(FlowProvider, createFlowProvider);
|
|
605
|
+
|
|
606
|
+
// Default eventEmitter to webSocketEventEmitter with the provided eventBroadcaster
|
|
607
|
+
const finalEventEmitter =
|
|
608
|
+
eventEmitter ?? webSocketEventEmitter(eventBroadcaster);
|
|
609
|
+
|
|
610
|
+
// Convert DataStoreConfig to Layer
|
|
611
|
+
const dataStoreLayer = await createDataStoreLayer(dataStore);
|
|
612
|
+
|
|
613
|
+
return createInternalExpressUploadistaAdapter<FlowReq, TPlugins>({
|
|
614
|
+
baseUrl,
|
|
615
|
+
flowProvider,
|
|
616
|
+
plugins,
|
|
617
|
+
eventEmitter: finalEventEmitter,
|
|
618
|
+
dataStore: dataStoreLayer,
|
|
619
|
+
bufferedDataStore,
|
|
620
|
+
kvStore,
|
|
621
|
+
generateId,
|
|
622
|
+
authMiddleware,
|
|
623
|
+
authCacheConfig,
|
|
624
|
+
metricsLayer,
|
|
625
|
+
});
|
|
626
|
+
};
|