@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.
Files changed (48) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +5 -0
  3. package/LICENSE +21 -0
  4. package/README.md +456 -0
  5. package/USAGE.md +164 -0
  6. package/dist/adapter-layer.d.ts +22 -0
  7. package/dist/adapter-layer.d.ts.map +1 -0
  8. package/dist/adapter-layer.js +3 -0
  9. package/dist/error-types.d.ts +24 -0
  10. package/dist/error-types.d.ts.map +1 -0
  11. package/dist/error-types.js +65 -0
  12. package/dist/flow-adapter.d.ts +19 -0
  13. package/dist/flow-adapter.d.ts.map +1 -0
  14. package/dist/flow-adapter.js +80 -0
  15. package/dist/flow-http-handlers.d.ts +9 -0
  16. package/dist/flow-http-handlers.d.ts.map +1 -0
  17. package/dist/flow-http-handlers.js +133 -0
  18. package/dist/http-handlers.d.ts +7 -0
  19. package/dist/http-handlers.d.ts.map +1 -0
  20. package/dist/http-handlers.js +78 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +2 -0
  24. package/dist/upload-http-handlers.d.ts +9 -0
  25. package/dist/upload-http-handlers.d.ts.map +1 -0
  26. package/dist/upload-http-handlers.js +113 -0
  27. package/dist/uploadista-adapter-layer.d.ts +24 -0
  28. package/dist/uploadista-adapter-layer.d.ts.map +1 -0
  29. package/dist/uploadista-adapter-layer.js +4 -0
  30. package/dist/uploadista-adapter.d.ts +78 -0
  31. package/dist/uploadista-adapter.d.ts.map +1 -0
  32. package/dist/uploadista-adapter.js +297 -0
  33. package/dist/uploadista-websocket-handler.d.ts +9 -0
  34. package/dist/uploadista-websocket-handler.d.ts.map +1 -0
  35. package/dist/uploadista-websocket-handler.js +132 -0
  36. package/dist/websocket-handler.d.ts +8 -0
  37. package/dist/websocket-handler.d.ts.map +1 -0
  38. package/dist/websocket-handler.js +82 -0
  39. package/package.json +40 -0
  40. package/src/error-types.ts +103 -0
  41. package/src/flow-http-handlers.ts +184 -0
  42. package/src/index.ts +14 -0
  43. package/src/upload-http-handlers.ts +186 -0
  44. package/src/uploadista-adapter-layer.ts +32 -0
  45. package/src/uploadista-adapter.ts +626 -0
  46. package/src/uploadista-websocket-handler.ts +209 -0
  47. package/tsconfig.json +11 -0
  48. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,78 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { EventBroadcasterService, UploadFileDataStores, UploadistaError } from "@uploadista/core";
3
+ import { type Flow, FlowProvider, FlowServer } from "@uploadista/core/flow";
4
+ import { type BaseEventEmitterService, type BaseKvStoreService, type DataStoreConfig, type UploadFileDataStore, type UploadFileKVStore } from "@uploadista/core/types";
5
+ import { UploadServer } from "@uploadista/core/upload";
6
+ import { type GenerateId } from "@uploadista/core/utils";
7
+ import { type MetricsService } from "@uploadista/observability";
8
+ import { type AuthCacheConfig, type AuthResult } from "@uploadista/server";
9
+ import { Effect, Layer } from "effect";
10
+ import type { Request, Response } from "express";
11
+ import type { z } from "zod";
12
+ import { type WebSocketConnection, type WebSocketHandlers } from "./uploadista-adapter-layer";
13
+ export type ExpressUploadistaAdapterOptions<TFlows extends (flowId: string, clientId: string | null) => Effect.Effect<Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>, UploadistaError, unknown> = any, TPlugins extends readonly Layer.Layer<any, never, never>[] = Layer.Layer<any, never, never>[]> = {
14
+ flows: TFlows;
15
+ plugins?: TPlugins;
16
+ dataStore: DataStoreConfig;
17
+ bufferedDataStore?: Layer.Layer<UploadFileDataStore, never, UploadFileKVStore>;
18
+ baseUrl?: string;
19
+ kvStore: Layer.Layer<BaseKvStoreService>;
20
+ eventEmitter?: Layer.Layer<BaseEventEmitterService>;
21
+ eventBroadcaster?: Layer.Layer<EventBroadcasterService>;
22
+ generateId?: Layer.Layer<GenerateId>;
23
+ withTracing?: boolean;
24
+ authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>;
25
+ authCacheConfig?: AuthCacheConfig;
26
+ metricsLayer?: Layer.Layer<MetricsService, never, never>;
27
+ };
28
+ export type InternalExpressUploadistaAdapterOptions<TRequirements = never, TPlugins extends readonly Layer.Layer<TRequirements, never, never>[] = []> = {
29
+ flowProvider: Layer.Layer<FlowProvider>;
30
+ plugins?: TPlugins;
31
+ dataStore: Layer.Layer<UploadFileDataStores, never, UploadFileKVStore>;
32
+ bufferedDataStore?: Layer.Layer<UploadFileDataStore, never, UploadFileKVStore>;
33
+ baseUrl: string;
34
+ kvStore: Layer.Layer<BaseKvStoreService>;
35
+ eventEmitter: Layer.Layer<BaseEventEmitterService>;
36
+ generateId?: Layer.Layer<GenerateId>;
37
+ withTracing?: boolean;
38
+ authMiddleware?: (req: Request, res: Response) => Promise<AuthResult>;
39
+ authCacheConfig?: AuthCacheConfig;
40
+ metricsLayer?: Layer.Layer<MetricsService, never, never>;
41
+ };
42
+ export type ExpressUploadistaAdapter = {
43
+ baseUrl: string;
44
+ handler: (req: Request, res: Response, next?: (error?: Error) => void) => void;
45
+ websocketHandler: (req: IncomingMessage, connection: WebSocketConnection) => WebSocketHandlers;
46
+ websocketConnectionHandler: (ws: WebSocket, req: IncomingMessage) => void;
47
+ };
48
+ interface WebSocket {
49
+ readyState: number;
50
+ OPEN: number;
51
+ send: (data: string) => void;
52
+ close: (code?: number, reason?: string) => void;
53
+ on: (event: string, handler: (...args: any[]) => void) => void;
54
+ }
55
+ export type ExpressUploadistaServer = {
56
+ handler: (req: Request, res: Response) => Effect.Effect<void, never, never>;
57
+ uploadServer: Layer.Layer<UploadServer>;
58
+ flowServer: Layer.Layer<FlowServer>;
59
+ websocketHandler: (req: IncomingMessage, connection: WebSocketConnection) => WebSocketHandlers;
60
+ };
61
+ /**
62
+ * Creates an Effect-native unified Express server - combining upload and flow capabilities
63
+ */
64
+ export declare const createExpressUploadistaServer: <TRequirements = UploadServer>({ baseUrl, flowProvider, eventEmitter, dataStore, bufferedDataStore, kvStore, generateId, authMiddleware, authCacheConfig, metricsLayer, }: InternalExpressUploadistaAdapterOptions<TRequirements>) => Effect.Effect<ExpressUploadistaServer>;
65
+ /**
66
+ * Creates a Promise-based Express adapter for compatibility with existing Express applications
67
+ * This wraps the Effect-native version with Promise conversion and caches the server instance
68
+ */
69
+ export declare const createInternalExpressUploadistaAdapter: <TRequirements = UploadServer, TPlugins extends readonly Layer.Layer<TRequirements, never, never>[] = []>({ baseUrl, flowProvider, plugins, eventEmitter, dataStore, bufferedDataStore, kvStore, withTracing, authMiddleware, authCacheConfig, metricsLayer, }: InternalExpressUploadistaAdapterOptions<TRequirements, TPlugins>) => Promise<ExpressUploadistaAdapter>;
70
+ /**
71
+ * Creates a Promise-based Uploadista Express adapter for compatibility
72
+ *
73
+ * Note: Ensure that the plugins array provides all services required by your flows.
74
+ * Missing plugin services will result in runtime errors during flow execution.
75
+ */
76
+ export declare const createExpressUploadistaAdapter: <TFlows extends (flowId: string, clientId: string | null) => Effect.Effect<Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>, UploadistaError, unknown> = any, TPlugins extends readonly Layer.Layer<any, never, never>[] = Layer.Layer<any, never, never>[]>({ baseUrl, flows, plugins, eventEmitter, eventBroadcaster, dataStore, bufferedDataStore, kvStore, generateId, authMiddleware, authCacheConfig, metricsLayer, }: ExpressUploadistaAdapterOptions<TFlows, TPlugins>) => Promise<ExpressUploadistaAdapter>;
77
+ export {};
78
+ //# sourceMappingURL=uploadista-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadista-adapter.d.ts","sourceRoot":"","sources":["../src/uploadista-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,297 @@
1
+ import { FlowProvider, FlowServer } from "@uploadista/core/flow";
2
+ import { createDataStoreLayer, } from "@uploadista/core/types";
3
+ import { UploadServer } from "@uploadista/core/upload";
4
+ import { GenerateIdLive } from "@uploadista/core/utils";
5
+ import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
6
+ import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
7
+ import { NodeSdkLive, NoOpMetricsServiceLive, } from "@uploadista/observability";
8
+ import { AuthCacheServiceLive, AuthContextServiceLive, createFlowServerLayer, createUploadServerLayer, } from "@uploadista/server";
9
+ import { Effect, Layer } from "effect";
10
+ import { handleContinueFlow, handleFlowGet, handleFlowPost, handleJobStatus, } from "./flow-http-handlers";
11
+ import { handleUploadGet, handleUploadPatch, handleUploadPost, } from "./upload-http-handlers";
12
+ import { ExpressUploadistaAdapterService, } from "./uploadista-adapter-layer";
13
+ import { createUploadistaWebSocketHandler } from "./uploadista-websocket-handler";
14
+ // Effect-based service factory for creating the unified adapter layer
15
+ const createExpressUploadistaAdapterServiceLayer = (baseUrl, authMiddleware, authCacheConfig, metricsLayer) => Layer.effect(ExpressUploadistaAdapterService, Effect.gen(function* () {
16
+ const uploadServer = yield* UploadServer;
17
+ const flowServer = yield* FlowServer;
18
+ // Create auth cache layer (always present, even if auth is not enabled)
19
+ const authCacheLayer = AuthCacheServiceLive(authCacheConfig);
20
+ return {
21
+ handler: (req, res) => Effect.gen(function* () {
22
+ // Call auth middleware if configured and create auth context layer
23
+ let authContext = null;
24
+ if (authMiddleware) {
25
+ // Run auth middleware with timeout protection (5 seconds default)
26
+ const authMiddlewareWithTimeout = Effect.tryPromise({
27
+ try: () => authMiddleware(req, res),
28
+ catch: (error) => {
29
+ console.error("Auth middleware error:", error);
30
+ return { _tag: "AuthError", error };
31
+ },
32
+ }).pipe(Effect.timeout("5 seconds"), Effect.catchAll((error) => {
33
+ // Check if timeout occurred
34
+ if (error && typeof error === "object" && "_tag" in error) {
35
+ if (error._tag === "TimeoutException") {
36
+ console.error("Auth middleware timeout exceeded (5 seconds)");
37
+ return Effect.succeed({
38
+ _tag: "TimeoutError",
39
+ });
40
+ }
41
+ }
42
+ return Effect.succeed(null);
43
+ }));
44
+ const authResult = yield* authMiddlewareWithTimeout;
45
+ // If auth middleware timed out, return 503 Service Unavailable
46
+ if (authResult &&
47
+ typeof authResult === "object" &&
48
+ "_tag" in authResult &&
49
+ authResult._tag === "TimeoutError") {
50
+ res.status(503).json({
51
+ error: "Authentication service unavailable",
52
+ message: "Authentication took too long to respond. Please try again.",
53
+ });
54
+ return;
55
+ }
56
+ // If auth middleware returned null, authentication failed
57
+ if (authResult === null) {
58
+ res.status(401).json({
59
+ error: "Unauthorized",
60
+ message: "Invalid credentials",
61
+ });
62
+ return;
63
+ }
64
+ // Check for error marker (shouldn't happen after catchAll, but for type safety)
65
+ if (authResult &&
66
+ typeof authResult === "object" &&
67
+ "_tag" in authResult &&
68
+ authResult._tag === "AuthError") {
69
+ res.status(500).json({
70
+ error: "Internal Server Error",
71
+ message: "An error occurred during authentication",
72
+ });
73
+ return;
74
+ }
75
+ authContext = authResult;
76
+ }
77
+ // Create auth context layer for this request
78
+ const authContextLayer = AuthContextServiceLive(authContext);
79
+ // Combine auth context, auth cache, and metrics layers
80
+ // Always provide a metrics layer (either real or no-op) to satisfy type requirements
81
+ const authLayer = Layer.mergeAll(authContextLayer, authCacheLayer, metricsLayer ?? NoOpMetricsServiceLive);
82
+ const url = new URL(req.url, `http://${req.get("host")}`);
83
+ // Check for uploadista/api/ prefix
84
+ if (!url.pathname.includes(`${baseUrl}/api/`)) {
85
+ res.status(404).json({ error: "Not found" });
86
+ return;
87
+ }
88
+ // Remove the prefix and get the actual route segments
89
+ const routeSegments = url.pathname
90
+ .replace(`${baseUrl}/api/`, "")
91
+ .split("/")
92
+ .filter(Boolean);
93
+ // Parse JSON body for routes that need it
94
+ const needsJsonBody = (routeSegments.includes("upload") && req.method === "POST") ||
95
+ (routeSegments.includes("flow") && req.method === "POST") ||
96
+ (routeSegments.includes("continue") &&
97
+ req.get("Content-Type")?.includes("application/json"));
98
+ if (needsJsonBody && !req.body) {
99
+ // Manually parse JSON body
100
+ yield* Effect.tryPromise({
101
+ try: async () => {
102
+ const chunks = [];
103
+ for await (const chunk of req) {
104
+ chunks.push(chunk);
105
+ }
106
+ const body = Buffer.concat(chunks).toString();
107
+ req.body = JSON.parse(body);
108
+ },
109
+ catch: (error) => error,
110
+ });
111
+ }
112
+ // Route based on path
113
+ if (routeSegments.includes("upload")) {
114
+ // Upload API routes - these now create jobs behind the scenes
115
+ switch (req.method) {
116
+ case "POST":
117
+ return yield* handleUploadPost(req, res, uploadServer).pipe(Effect.provide(authLayer));
118
+ case "GET":
119
+ return yield* handleUploadGet(req, res, uploadServer).pipe(Effect.provide(authLayer));
120
+ case "PATCH":
121
+ return yield* handleUploadPatch(req, res, uploadServer).pipe(Effect.provide(authLayer));
122
+ default:
123
+ res.status(405).json({ error: "Method not allowed" });
124
+ return;
125
+ }
126
+ }
127
+ else if (routeSegments.includes("flow")) {
128
+ // Flow API routes
129
+ switch (req.method) {
130
+ case "GET":
131
+ return yield* handleFlowGet(req, res, flowServer).pipe(Effect.provide(authLayer));
132
+ case "POST":
133
+ return yield* handleFlowPost(req, res, flowServer).pipe(Effect.provide(authLayer));
134
+ default:
135
+ res.status(405).json({ error: "Method not allowed" });
136
+ return;
137
+ }
138
+ }
139
+ else if (routeSegments.includes("jobs")) {
140
+ // Unified job status routes
141
+ if (req.method === "GET" && url.pathname.endsWith("/status")) {
142
+ return yield* handleJobStatus(req, res, flowServer).pipe(Effect.provide(authLayer));
143
+ }
144
+ else if (req.method === "PATCH" &&
145
+ routeSegments.includes("continue")) {
146
+ return yield* handleContinueFlow(req, res, flowServer).pipe(Effect.provide(authLayer));
147
+ }
148
+ res.status(405).json({ error: "Method not allowed" });
149
+ return;
150
+ }
151
+ else {
152
+ res.status(404).json({ error: "Not found" });
153
+ return;
154
+ }
155
+ }).pipe(Effect.catchAll(() => Effect.sync(() => {
156
+ res.status(500).json({ error: "Internal server error" });
157
+ }))),
158
+ websocketHandler: createUploadistaWebSocketHandler(baseUrl, uploadServer, flowServer),
159
+ };
160
+ }));
161
+ /**
162
+ * Creates an Effect-native unified Express server - combining upload and flow capabilities
163
+ */
164
+ export const createExpressUploadistaServer = ({ baseUrl, flowProvider, eventEmitter, dataStore, bufferedDataStore, kvStore, generateId = GenerateIdLive, authMiddleware, authCacheConfig, metricsLayer, }) => {
165
+ // Set up upload server dependencies using shared utility
166
+ const uploadServerLayer = createUploadServerLayer({
167
+ kvStore,
168
+ eventEmitter,
169
+ dataStore,
170
+ bufferedDataStore,
171
+ generateId,
172
+ });
173
+ // Set up flow server dependencies using shared utility
174
+ const flowServerLayer = createFlowServerLayer({
175
+ kvStore,
176
+ eventEmitter,
177
+ flowProvider,
178
+ uploadServer: uploadServerLayer,
179
+ });
180
+ // Set up adapter
181
+ const adapterLayer = Layer.provide(createExpressUploadistaAdapterServiceLayer(baseUrl, authMiddleware, authCacheConfig, metricsLayer), Layer.mergeAll(uploadServerLayer, flowServerLayer));
182
+ return Effect.gen(function* () {
183
+ const adapterService = yield* ExpressUploadistaAdapterService;
184
+ return {
185
+ handler: (req, res) => adapterService.handler(req, res),
186
+ websocketHandler: (req, connection) => adapterService.websocketHandler(req, connection),
187
+ uploadServer: uploadServerLayer,
188
+ flowServer: flowServerLayer,
189
+ };
190
+ }).pipe(Effect.provide(adapterLayer));
191
+ };
192
+ const runProgram = (effect, withTracing) => {
193
+ if (withTracing) {
194
+ return Effect.runPromise(effect.pipe(Effect.provide(NodeSdkLive)));
195
+ }
196
+ return Effect.runPromise(effect);
197
+ };
198
+ /**
199
+ * Creates a Promise-based Express adapter for compatibility with existing Express applications
200
+ * This wraps the Effect-native version with Promise conversion and caches the server instance
201
+ */
202
+ export const createInternalExpressUploadistaAdapter = async ({ baseUrl, flowProvider, plugins, eventEmitter, dataStore, bufferedDataStore, kvStore, withTracing = false, authMiddleware, authCacheConfig, metricsLayer, }) => {
203
+ // Create and cache the Effect server instance
204
+ const uploadistaServer = await Effect.runPromise(createExpressUploadistaServer({
205
+ baseUrl,
206
+ flowProvider,
207
+ eventEmitter,
208
+ dataStore,
209
+ bufferedDataStore,
210
+ kvStore,
211
+ authMiddleware,
212
+ authCacheConfig,
213
+ metricsLayer,
214
+ }));
215
+ // Merge all plugin layers so we can provide them when running handlers
216
+ const pluginLayers = Layer.mergeAll(uploadistaServer.uploadServer, ...(plugins ?? []));
217
+ return {
218
+ baseUrl,
219
+ handler: (req, res, next) => {
220
+ runProgram(uploadistaServer.handler(req, res).pipe(Effect.provide(pluginLayers)), withTracing).catch((error) => {
221
+ console.error("Express adapter error:", error);
222
+ if (next)
223
+ next(error);
224
+ });
225
+ },
226
+ websocketHandler: (req, connection) => uploadistaServer.websocketHandler(req, connection),
227
+ websocketConnectionHandler: (ws, req) => {
228
+ // Filter to only handle uploadista WebSocket paths
229
+ if (!req.url?.startsWith(`/${baseUrl}/ws/`)) {
230
+ ws.close(1008, "Invalid WebSocket path");
231
+ return;
232
+ }
233
+ console.log(`📡 WebSocket connected: ${req.url}`);
234
+ const connection = {
235
+ id: `conn_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
236
+ send: (data) => {
237
+ if (ws.readyState === ws.OPEN) {
238
+ ws.send(data);
239
+ }
240
+ },
241
+ close: (code, reason) => ws.close(code, reason),
242
+ readyState: ws.readyState,
243
+ };
244
+ // Initialize WebSocket handler with Uploadista and get event handlers
245
+ const handlers = uploadistaServer.websocketHandler(req, connection);
246
+ // Attach event handlers
247
+ ws.on("message", (message) => {
248
+ handlers.onMessage(message.toString());
249
+ });
250
+ ws.on("close", () => {
251
+ handlers.onClose();
252
+ });
253
+ ws.on("error", (error) => {
254
+ handlers.onError(error);
255
+ });
256
+ },
257
+ };
258
+ };
259
+ /**
260
+ * Creates a Promise-based Uploadista Express adapter for compatibility
261
+ *
262
+ * Note: Ensure that the plugins array provides all services required by your flows.
263
+ * Missing plugin services will result in runtime errors during flow execution.
264
+ */
265
+ export const createExpressUploadistaAdapter = async ({ baseUrl = "uploadista", flows,
266
+ // Default to an empty plugin list while preserving the generic type
267
+ plugins = [], eventEmitter, eventBroadcaster = memoryEventBroadcaster, dataStore, bufferedDataStore, kvStore, generateId = GenerateIdLive, authMiddleware, authCacheConfig, metricsLayer, }) => {
268
+ // Create a simplified flow provider that uses the flows function directly
269
+ const createFlowProvider = Effect.succeed({
270
+ getFlow: (flowId, clientId) => {
271
+ // The flows function returns an Effect with TRequirements context,
272
+ // but the FlowProvider interface expects no context.
273
+ // We cast this to match the interface - the requirements will be provided
274
+ // at the layer level when the flow adapter is created.
275
+ return flows(flowId, clientId);
276
+ },
277
+ });
278
+ // Create the flow provider layer that provides the requirements
279
+ const flowProvider = Layer.effect(FlowProvider, createFlowProvider);
280
+ // Default eventEmitter to webSocketEventEmitter with the provided eventBroadcaster
281
+ const finalEventEmitter = eventEmitter ?? webSocketEventEmitter(eventBroadcaster);
282
+ // Convert DataStoreConfig to Layer
283
+ const dataStoreLayer = await createDataStoreLayer(dataStore);
284
+ return createInternalExpressUploadistaAdapter({
285
+ baseUrl,
286
+ flowProvider,
287
+ plugins,
288
+ eventEmitter: finalEventEmitter,
289
+ dataStore: dataStoreLayer,
290
+ bufferedDataStore,
291
+ kvStore,
292
+ generateId,
293
+ authMiddleware,
294
+ authCacheConfig,
295
+ metricsLayer,
296
+ });
297
+ };
@@ -0,0 +1,9 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { FlowServerShape } from "@uploadista/core/flow";
3
+ import type { UploadServerShape } from "@uploadista/core/upload";
4
+ import type { WebSocketConnection, WebSocketHandlers } from "./uploadista-adapter-layer";
5
+ export declare const createUploadistaWebSocketHandler: (baseUrl: string, uploadServer: UploadServerShape, flowServer: FlowServerShape) => (req: IncomingMessage, connection: WebSocketConnection) => WebSocketHandlers;
6
+ export declare const createWebSocketMessageHandler: (_uploadServer: UploadServerShape, _flowServer: FlowServerShape, _uploadId: string | undefined, _jobId: string | undefined, connection: WebSocketConnection) => (message: string) => void;
7
+ export declare const createWebSocketCloseHandler: (uploadServer: UploadServerShape, flowServer: FlowServerShape, uploadId: string | undefined, jobId: string | undefined) => () => void;
8
+ export declare const createWebSocketErrorHandler: (eventId: string | undefined) => (error: Error) => void;
9
+ //# sourceMappingURL=uploadista-websocket-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uploadista-websocket-handler.d.ts","sourceRoot":"","sources":["../src/uploadista-websocket-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EACV,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AAEpC,eAAO,MAAM,gCAAgC,GAC3C,SAAS,MAAM,EACf,cAAc,iBAAiB,EAC/B,YAAY,eAAe,MAGzB,KAAK,eAAe,EACpB,YAAY,mBAAmB,KAC9B,iBAoHJ,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,eAAe,iBAAiB,EAChC,aAAa,eAAe,EAC5B,WAAW,MAAM,GAAG,SAAS,EAC7B,QAAQ,MAAM,GAAG,SAAS,EAC1B,YAAY,mBAAmB,MAEvB,SAAS,MAAM,KAAG,IAqB3B,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACtC,cAAc,iBAAiB,EAC/B,YAAY,eAAe,EAC3B,UAAU,MAAM,GAAG,SAAS,EAC5B,OAAO,MAAM,GAAG,SAAS,WAEd,IAwBZ,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,SAAS,MAAM,GAAG,SAAS,MAC7D,OAAO,KAAK,KAAG,IAGxB,CAAC"}
@@ -0,0 +1,132 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { Effect } from "effect";
3
+ export const createUploadistaWebSocketHandler = (baseUrl, uploadServer, flowServer) => {
4
+ return (req, connection) => {
5
+ // Check for ws/uploadista prefix
6
+ const url = req.url || "";
7
+ if (!url.includes(`${baseUrl}/ws/`)) {
8
+ connection.send(JSON.stringify({
9
+ type: "error",
10
+ message: `WebSocket path must start with ${baseUrl}/ws/`,
11
+ }));
12
+ connection.close(1000, "Invalid path");
13
+ // Return no-op handlers since connection is closed
14
+ return {
15
+ onMessage: () => { },
16
+ onClose: () => { },
17
+ onError: () => { },
18
+ };
19
+ }
20
+ // Remove the prefix and get the actual route segments
21
+ const routeSegments = url
22
+ .replace(`${baseUrl}/ws/`, "")
23
+ .split("/")
24
+ .filter(Boolean);
25
+ const isUploadRoute = routeSegments.includes("upload");
26
+ const isFlowRoute = routeSegments.includes("flow");
27
+ // Extract jobId and uploadId from URL path or query parameters
28
+ // Path format: /uploadista/ws/flow/{jobId} or /uploadista/ws/upload/{uploadId}
29
+ let jobId = extractQueryParam(url, "jobId");
30
+ let uploadId = extractQueryParam(url, "uploadId");
31
+ // If not in query params, extract from path segments
32
+ if (!jobId && !uploadId && routeSegments.length >= 2) {
33
+ const routeType = routeSegments[0]; // 'flow' or 'upload'
34
+ const id = routeSegments[1]; // the actual ID
35
+ if (routeType === "flow") {
36
+ jobId = id;
37
+ }
38
+ else if (routeType === "upload") {
39
+ uploadId = id;
40
+ }
41
+ }
42
+ // Use jobId if available, otherwise use uploadId
43
+ const eventId = jobId || uploadId;
44
+ console.log("Uploadista websocket handler", { jobId, uploadId, eventId });
45
+ const subscribeEffect = Effect.gen(function* () {
46
+ // Subscribe to flow events if we had a jobId
47
+ if (isFlowRoute && jobId) {
48
+ // Subscribe to flow events (this handles job tracking)
49
+ yield* flowServer.subscribeToFlowEvents(jobId, connection);
50
+ }
51
+ // If we have an uploadId, also subscribe to upload events
52
+ // These will be treated as task events within the job
53
+ if (isUploadRoute && uploadId) {
54
+ yield* uploadServer.subscribeToUploadEvents(uploadId, connection);
55
+ }
56
+ connection.send(JSON.stringify({
57
+ type: "connection",
58
+ message: "Uploadista WebSocket connected",
59
+ id: eventId,
60
+ jobId,
61
+ uploadId,
62
+ timestamp: new Date().toISOString(),
63
+ }));
64
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
65
+ console.error("Error subscribing to events:", error);
66
+ const errorMessage = error instanceof UploadistaError
67
+ ? error.body
68
+ : "Failed to subscribe to events";
69
+ connection.send(JSON.stringify({
70
+ type: "error",
71
+ message: errorMessage,
72
+ code: error instanceof UploadistaError
73
+ ? error.code
74
+ : "SUBSCRIPTION_ERROR",
75
+ }));
76
+ })));
77
+ Effect.runFork(subscribeEffect);
78
+ // Return handlers for WebSocket events
79
+ return {
80
+ onMessage: createWebSocketMessageHandler(uploadServer, flowServer, uploadId, jobId, connection),
81
+ onClose: createWebSocketCloseHandler(uploadServer, flowServer, uploadId, jobId),
82
+ onError: createWebSocketErrorHandler(eventId),
83
+ };
84
+ };
85
+ };
86
+ export const createWebSocketMessageHandler = (_uploadServer, _flowServer, _uploadId, _jobId, connection) => {
87
+ return (message) => {
88
+ try {
89
+ const parsed = JSON.parse(message);
90
+ if (parsed.type === "ping") {
91
+ connection.send(JSON.stringify({
92
+ type: "pong",
93
+ timestamp: new Date().toISOString(),
94
+ }));
95
+ }
96
+ }
97
+ catch (error) {
98
+ console.error("Error handling WebSocket message:", error);
99
+ connection.send(JSON.stringify({
100
+ type: "error",
101
+ message: "Invalid message format",
102
+ }));
103
+ }
104
+ };
105
+ };
106
+ export const createWebSocketCloseHandler = (uploadServer, flowServer, uploadId, jobId) => {
107
+ return () => {
108
+ const unsubscribeEffect = Effect.gen(function* () {
109
+ // Unsubscribe from flow events if we had a jobId
110
+ if (jobId) {
111
+ yield* flowServer.unsubscribeFromFlowEvents(jobId);
112
+ }
113
+ // Unsubscribe from upload events if we had an uploadId
114
+ if (uploadId) {
115
+ yield* uploadServer.unsubscribeFromUploadEvents(uploadId);
116
+ }
117
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
118
+ console.error("Error unsubscribing from events:", error instanceof UploadistaError ? error.body : error);
119
+ })));
120
+ Effect.runFork(unsubscribeEffect);
121
+ };
122
+ };
123
+ export const createWebSocketErrorHandler = (eventId) => {
124
+ return (error) => {
125
+ console.error(`WebSocket error for event ${eventId}:`, error);
126
+ };
127
+ };
128
+ function extractQueryParam(url, param) {
129
+ const regex = new RegExp(`[?&]${param}=([^&]*)`);
130
+ const match = url.match(regex);
131
+ return match?.[1] ? decodeURIComponent(match[1]) : undefined;
132
+ }
@@ -0,0 +1,8 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { UploadServerShape } from "@uploadista/core/upload";
3
+ import type { WebSocketConnection } from "./adapter-layer";
4
+ export declare const createWebSocketHandler: (server: UploadServerShape) => (req: IncomingMessage, connection: WebSocketConnection) => void;
5
+ export declare const createWebSocketMessageHandler: (_server: UploadServerShape, _uploadId: string) => (message: string, connection: WebSocketConnection) => void;
6
+ export declare const createWebSocketCloseHandler: (server: UploadServerShape, uploadId: string) => () => void;
7
+ export declare const createWebSocketErrorHandler: (uploadId: string) => (error: Error) => void;
8
+ //# sourceMappingURL=websocket-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-handler.d.ts","sourceRoot":"","sources":["../src/websocket-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,eAAO,MAAM,sBAAsB,GAAI,QAAQ,iBAAiB,MACtD,KAAK,eAAe,EAAE,YAAY,mBAAmB,KAAG,IAkDjE,CAAC;AAEF,eAAO,MAAM,6BAA6B,GACxC,SAAS,iBAAiB,EAC1B,WAAW,MAAM,MAET,SAAS,MAAM,EAAE,YAAY,mBAAmB,KAAG,IAqB5D,CAAC;AAEF,eAAO,MAAM,2BAA2B,GACtC,QAAQ,iBAAiB,EACzB,UAAU,MAAM,WAEL,IAgBZ,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,UAAU,MAAM,MAClD,OAAO,KAAK,KAAG,IAGxB,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { Effect } from "effect";
3
+ export const createWebSocketHandler = (server) => {
4
+ return (req, connection) => {
5
+ // Extract uploadId from request params or URL
6
+ const uploadId = req.url ? extractUploadIdFromUrl(req.url) : undefined;
7
+ if (!uploadId) {
8
+ connection.send(JSON.stringify({
9
+ type: "error",
10
+ message: "Missing uploadId parameter",
11
+ }));
12
+ connection.close(1000, "Missing uploadId");
13
+ return;
14
+ }
15
+ const subscribeEffect = Effect.gen(function* () {
16
+ yield* server.subscribeToUploadEvents(uploadId, connection);
17
+ connection.send(JSON.stringify({
18
+ type: "connection",
19
+ message: "Upload events WebSocket connected",
20
+ uploadId,
21
+ timestamp: new Date().toISOString(),
22
+ }));
23
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
24
+ console.error("Error subscribing to upload events:", error);
25
+ const errorMessage = error instanceof UploadistaError
26
+ ? error.body
27
+ : "Failed to subscribe to upload events";
28
+ connection.send(JSON.stringify({
29
+ type: "error",
30
+ message: errorMessage,
31
+ code: error instanceof UploadistaError
32
+ ? error.code
33
+ : "SUBSCRIPTION_ERROR",
34
+ }));
35
+ })));
36
+ Effect.runFork(subscribeEffect);
37
+ };
38
+ };
39
+ export const createWebSocketMessageHandler = (_server, _uploadId) => {
40
+ return (message, connection) => {
41
+ try {
42
+ const parsed = JSON.parse(message);
43
+ if (parsed.type === "ping") {
44
+ connection.send(JSON.stringify({
45
+ type: "pong",
46
+ timestamp: new Date().toISOString(),
47
+ }));
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error("Error handling WebSocket message:", error);
52
+ connection.send(JSON.stringify({
53
+ type: "error",
54
+ message: "Invalid message format",
55
+ }));
56
+ }
57
+ };
58
+ };
59
+ export const createWebSocketCloseHandler = (server, uploadId) => {
60
+ return () => {
61
+ const unsubscribeEffect = Effect.gen(function* () {
62
+ yield* server.unsubscribeFromUploadEvents(uploadId);
63
+ }).pipe(Effect.catchAll((error) => Effect.sync(() => {
64
+ console.error("Error unsubscribing from upload events:", error instanceof UploadistaError ? error.body : error);
65
+ })));
66
+ Effect.runFork(unsubscribeEffect);
67
+ };
68
+ };
69
+ export const createWebSocketErrorHandler = (uploadId) => {
70
+ return (error) => {
71
+ console.error(`WebSocket error for upload ${uploadId}:`, error);
72
+ };
73
+ };
74
+ function extractUploadIdFromUrl(url) {
75
+ const segments = url.split("/").filter(Boolean);
76
+ // Assuming URL structure like /api/upload/:uploadId/ws
77
+ const uploadIndex = segments.indexOf("upload");
78
+ if (uploadIndex !== -1 && uploadIndex + 1 < segments.length) {
79
+ return segments[uploadIndex + 1];
80
+ }
81
+ return undefined;
82
+ }