@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,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
|
+
}
|