@uploadista/server 0.0.7 → 0.0.9
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/CHANGELOG.md +69 -0
- package/dist/auth/index.d.mts +2 -0
- package/dist/auth/index.mjs +1 -0
- package/dist/{auth-C77S4vQd.js → auth-BqArZeGK.mjs} +1 -1
- package/dist/auth-BqArZeGK.mjs.map +1 -0
- package/dist/{index-CvDNB1lJ.d.ts → index--Lny6VJP.d.mts} +1 -1
- package/dist/index--Lny6VJP.d.mts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +735 -12
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1343 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +10 -7
- package/src/adapter/index.ts +10 -0
- package/src/adapter/types.ts +229 -0
- package/src/core/http-handlers/flow-http-handlers.ts +245 -0
- package/src/core/http-handlers/http-handlers.ts +72 -0
- package/src/core/http-handlers/upload-http-handlers.ts +168 -0
- package/src/core/index.ts +12 -0
- package/src/core/routes.ts +188 -0
- package/src/core/server.ts +327 -0
- package/src/core/types.ts +322 -0
- package/src/core/websocket-handlers/flow-websocket-handlers.ts +47 -0
- package/src/core/websocket-handlers/upload-websocket-handlers.ts +47 -0
- package/src/core/websocket-handlers/websocket-handlers.ts +151 -0
- package/src/core/websocket-routes.ts +136 -0
- package/src/index.ts +2 -0
- package/dist/auth/index.d.ts +0 -2
- package/dist/auth/index.js +0 -1
- package/dist/auth-C77S4vQd.js.map +0 -1
- package/dist/index-CvDNB1lJ.d.ts.map +0 -1
- package/dist/index.d.ts +0 -620
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { UploadistaError } from "@uploadista/core";
|
|
2
|
+
import { type Flow, FlowProvider, FlowWaitUntil } from "@uploadista/core/flow";
|
|
3
|
+
import {
|
|
4
|
+
createDataStoreLayer,
|
|
5
|
+
type UploadFileDataStores,
|
|
6
|
+
type UploadFileKVStore,
|
|
7
|
+
} from "@uploadista/core/types";
|
|
8
|
+
import { GenerateIdLive } from "@uploadista/core/utils";
|
|
9
|
+
import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
10
|
+
import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
11
|
+
import { NodeSdkLive, NoOpMetricsServiceLive } from "@uploadista/observability";
|
|
12
|
+
import { Effect, Layer, ManagedRuntime } from "effect";
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
import type { StandardResponse } from "../adapter";
|
|
15
|
+
import { AuthCacheServiceLive } from "../cache";
|
|
16
|
+
import { handleFlowError } from "../http-utils";
|
|
17
|
+
import { createFlowServerLayer, createUploadServerLayer } from "../layer-utils";
|
|
18
|
+
import { AuthContextServiceLive } from "../service";
|
|
19
|
+
import type { AuthContext } from "../types";
|
|
20
|
+
import { handleUploadistaRequest } from "./http-handlers/http-handlers";
|
|
21
|
+
import type { NotFoundResponse } from "./routes";
|
|
22
|
+
import type { UploadistaServer, UploadistaServerConfig } from "./types";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates the unified Uploadista server with framework-specific adapter.
|
|
26
|
+
*
|
|
27
|
+
* This function composes all layers (upload server, flow server, auth, metrics)
|
|
28
|
+
* and returns a handler that works with any framework via the provided adapter.
|
|
29
|
+
*
|
|
30
|
+
* The core server handles:
|
|
31
|
+
* - Route parsing and matching
|
|
32
|
+
* - Auth middleware execution with timeout protection
|
|
33
|
+
* - Layer composition (upload/flow servers, auth cache, metrics)
|
|
34
|
+
* - Error handling and response formatting
|
|
35
|
+
* - Effect program execution with optional tracing
|
|
36
|
+
*
|
|
37
|
+
* @param config - Server configuration including adapter and business logic
|
|
38
|
+
* @returns Object with handler function, optional WebSocket handler, and base URL
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { createUploadistaServer, honoAdapter } from "@uploadista/server";
|
|
43
|
+
*
|
|
44
|
+
* const server = await createUploadistaServer({
|
|
45
|
+
* flows: getFlowById,
|
|
46
|
+
* dataStore: { type: "s3", config: { bucket: "uploads" } },
|
|
47
|
+
* kvStore: redisKvStore,
|
|
48
|
+
* adapter: honoAdapter({
|
|
49
|
+
* authMiddleware: async (c) => ({ clientId: "user-123" })
|
|
50
|
+
* })
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // Use with Hono
|
|
54
|
+
* app.all("/uploadista/*", server.handler);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export const createUploadistaServer = async <
|
|
58
|
+
TContext,
|
|
59
|
+
TResponse,
|
|
60
|
+
TWebSocketHandler = unknown,
|
|
61
|
+
>({
|
|
62
|
+
flows,
|
|
63
|
+
dataStore,
|
|
64
|
+
kvStore,
|
|
65
|
+
plugins = [],
|
|
66
|
+
eventEmitter,
|
|
67
|
+
eventBroadcaster = memoryEventBroadcaster,
|
|
68
|
+
withTracing = false,
|
|
69
|
+
baseUrl: configBaseUrl = "uploadista",
|
|
70
|
+
generateId = GenerateIdLive,
|
|
71
|
+
metricsLayer,
|
|
72
|
+
bufferedDataStore,
|
|
73
|
+
adapter,
|
|
74
|
+
authCacheConfig,
|
|
75
|
+
}: UploadistaServerConfig<TContext, TResponse, TWebSocketHandler>): Promise<
|
|
76
|
+
UploadistaServer<TContext, TResponse, TWebSocketHandler>
|
|
77
|
+
> => {
|
|
78
|
+
// Default eventEmitter to webSocketEventEmitter with the provided eventBroadcaster
|
|
79
|
+
const finalEventEmitter =
|
|
80
|
+
eventEmitter ?? webSocketEventEmitter(eventBroadcaster);
|
|
81
|
+
|
|
82
|
+
// Normalize baseUrl (remove trailing slash)
|
|
83
|
+
const baseUrl = configBaseUrl.endsWith("/")
|
|
84
|
+
? configBaseUrl.slice(0, -1)
|
|
85
|
+
: configBaseUrl;
|
|
86
|
+
|
|
87
|
+
// Create flow provider layer from flows function
|
|
88
|
+
const flowProviderLayer = Layer.effect(
|
|
89
|
+
FlowProvider,
|
|
90
|
+
Effect.succeed({
|
|
91
|
+
getFlow: (flowId: string, clientId: string | null) => {
|
|
92
|
+
// Cast the flows function to match FlowProvider expectations
|
|
93
|
+
// The context requirements will be provided at the layer level
|
|
94
|
+
return flows(flowId, clientId) as Effect.Effect<
|
|
95
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>,
|
|
96
|
+
UploadistaError,
|
|
97
|
+
never
|
|
98
|
+
>;
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Validate that eventEmitter is provided (required for upload/flow servers)
|
|
104
|
+
if (!finalEventEmitter) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"eventEmitter is required. Provide an event emitter layer in the configuration.",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create data store layer
|
|
111
|
+
const dataStoreLayer: Layer.Layer<
|
|
112
|
+
UploadFileDataStores,
|
|
113
|
+
never,
|
|
114
|
+
UploadFileKVStore
|
|
115
|
+
> = await createDataStoreLayer(dataStore);
|
|
116
|
+
|
|
117
|
+
// Create upload server layer
|
|
118
|
+
const uploadServerLayer = createUploadServerLayer({
|
|
119
|
+
kvStore,
|
|
120
|
+
eventEmitter: finalEventEmitter,
|
|
121
|
+
dataStore: dataStoreLayer,
|
|
122
|
+
bufferedDataStore,
|
|
123
|
+
generateId,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Create flow server layer
|
|
127
|
+
const flowServerLayer = createFlowServerLayer({
|
|
128
|
+
kvStore,
|
|
129
|
+
eventEmitter: finalEventEmitter,
|
|
130
|
+
flowProvider: flowProviderLayer,
|
|
131
|
+
uploadServer: uploadServerLayer,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Create auth cache layer (always present, even if auth is not enabled)
|
|
135
|
+
const authCacheLayer = AuthCacheServiceLive(authCacheConfig);
|
|
136
|
+
|
|
137
|
+
// Metrics layer (defaults to NoOp if not provided)
|
|
138
|
+
const effectiveMetricsLayer = metricsLayer ?? NoOpMetricsServiceLive;
|
|
139
|
+
|
|
140
|
+
const serverLayer = Layer.mergeAll(
|
|
141
|
+
uploadServerLayer,
|
|
142
|
+
flowServerLayer,
|
|
143
|
+
effectiveMetricsLayer,
|
|
144
|
+
authCacheLayer,
|
|
145
|
+
...plugins,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Create a shared managed runtime from the server layer
|
|
149
|
+
// This ensures all requests use the same layer instances (including event broadcaster)
|
|
150
|
+
// ManagedRuntime properly handles scoped resources and provides convenient run methods
|
|
151
|
+
const managedRuntime = ManagedRuntime.make(serverLayer);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Main request handler that processes HTTP requests through the adapter.
|
|
155
|
+
* Delegates to adapter's httpHandler if provided, otherwise uses standard flow.
|
|
156
|
+
*/
|
|
157
|
+
const handler = async <TRequirements>(ctx: TContext) => {
|
|
158
|
+
// Fallback: Standard routing logic (for adapters without httpHandler)
|
|
159
|
+
const program = Effect.gen(function* () {
|
|
160
|
+
// Extract standard request from framework-specific request
|
|
161
|
+
const uploadistaRequest = yield* adapter.extractRequest(ctx, { baseUrl });
|
|
162
|
+
|
|
163
|
+
// Run auth middleware if provided
|
|
164
|
+
let authContext: AuthContext | null = null;
|
|
165
|
+
if (adapter.runAuthMiddleware) {
|
|
166
|
+
const authMiddlewareWithTimeout = adapter.runAuthMiddleware(ctx).pipe(
|
|
167
|
+
Effect.timeout("5 seconds"),
|
|
168
|
+
Effect.catchAll(() => {
|
|
169
|
+
// Timeout error
|
|
170
|
+
console.error("Auth middleware timeout exceeded (5 seconds)");
|
|
171
|
+
return Effect.succeed({
|
|
172
|
+
_tag: "TimeoutError" as const,
|
|
173
|
+
} as const);
|
|
174
|
+
}),
|
|
175
|
+
Effect.catchAllCause((cause) => {
|
|
176
|
+
// Other errors
|
|
177
|
+
console.error("Auth middleware error:", cause);
|
|
178
|
+
return Effect.succeed({
|
|
179
|
+
_tag: "AuthError" as const,
|
|
180
|
+
error: cause,
|
|
181
|
+
} as const);
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const authResult:
|
|
186
|
+
| AuthContext
|
|
187
|
+
| null
|
|
188
|
+
| { _tag: "TimeoutError" }
|
|
189
|
+
| { _tag: "AuthError"; error: unknown } =
|
|
190
|
+
yield* authMiddlewareWithTimeout;
|
|
191
|
+
|
|
192
|
+
// Handle timeout
|
|
193
|
+
if (
|
|
194
|
+
authResult &&
|
|
195
|
+
typeof authResult === "object" &&
|
|
196
|
+
"_tag" in authResult &&
|
|
197
|
+
authResult._tag === "TimeoutError"
|
|
198
|
+
) {
|
|
199
|
+
const errorResponse: StandardResponse = {
|
|
200
|
+
status: 503,
|
|
201
|
+
headers: { "Content-Type": "application/json" },
|
|
202
|
+
body: {
|
|
203
|
+
error: "Authentication service unavailable",
|
|
204
|
+
message:
|
|
205
|
+
"Authentication took too long to respond. Please try again.",
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
return yield* adapter.sendResponse(errorResponse, ctx);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Handle auth error
|
|
212
|
+
if (
|
|
213
|
+
authResult &&
|
|
214
|
+
typeof authResult === "object" &&
|
|
215
|
+
"_tag" in authResult &&
|
|
216
|
+
authResult._tag === "AuthError"
|
|
217
|
+
) {
|
|
218
|
+
const errorResponse: StandardResponse = {
|
|
219
|
+
status: 500,
|
|
220
|
+
headers: { "Content-Type": "application/json" },
|
|
221
|
+
body: {
|
|
222
|
+
error: "Internal Server Error",
|
|
223
|
+
message: "An error occurred during authentication",
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
return yield* adapter.sendResponse(errorResponse, ctx);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Handle authentication failure (null result)
|
|
230
|
+
if (authResult === null) {
|
|
231
|
+
const errorResponse: StandardResponse = {
|
|
232
|
+
status: 401,
|
|
233
|
+
headers: { "Content-Type": "application/json" },
|
|
234
|
+
body: {
|
|
235
|
+
error: "Unauthorized",
|
|
236
|
+
message: "Invalid credentials",
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
return yield* adapter.sendResponse(errorResponse, ctx);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
authContext = authResult;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Create auth context layer for this request
|
|
246
|
+
const authContextLayer = AuthContextServiceLive(authContext);
|
|
247
|
+
|
|
248
|
+
// Extract waitUntil callback if available (for Cloudflare Workers)
|
|
249
|
+
// This must be extracted per-request since it comes from the framework context
|
|
250
|
+
const waitUntilLayers: Layer.Layer<any, never, never>[] = [];
|
|
251
|
+
if (adapter.extractWaitUntil) {
|
|
252
|
+
const waitUntilCallback = adapter.extractWaitUntil(ctx);
|
|
253
|
+
if (waitUntilCallback) {
|
|
254
|
+
waitUntilLayers.push(Layer.succeed(FlowWaitUntil, waitUntilCallback));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Combine auth context, auth cache, metrics layers, plugins, and waitUntil
|
|
259
|
+
// This ensures that flow nodes have access to all required services
|
|
260
|
+
const requestContextLayer = Layer.mergeAll(
|
|
261
|
+
authContextLayer,
|
|
262
|
+
authCacheLayer,
|
|
263
|
+
effectiveMetricsLayer,
|
|
264
|
+
...plugins,
|
|
265
|
+
...waitUntilLayers,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Check for baseUrl/api/ prefix
|
|
269
|
+
if (uploadistaRequest.type === "not-found") {
|
|
270
|
+
const notFoundResponse: NotFoundResponse = {
|
|
271
|
+
type: "not-found",
|
|
272
|
+
status: 404,
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
body: { error: "Not found" },
|
|
275
|
+
};
|
|
276
|
+
return yield* adapter.sendResponse(notFoundResponse, ctx);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Handle the request
|
|
280
|
+
const response = yield* handleUploadistaRequest<TRequirements>(
|
|
281
|
+
uploadistaRequest,
|
|
282
|
+
).pipe(Effect.provide(requestContextLayer));
|
|
283
|
+
|
|
284
|
+
return yield* adapter.sendResponse(response, ctx);
|
|
285
|
+
}).pipe(
|
|
286
|
+
// Catch all errors and format them appropriately
|
|
287
|
+
Effect.catchAll((error: unknown) => {
|
|
288
|
+
const errorInfo = handleFlowError(error);
|
|
289
|
+
const errorBody: Record<string, unknown> = {
|
|
290
|
+
code: errorInfo.code,
|
|
291
|
+
message: errorInfo.message,
|
|
292
|
+
};
|
|
293
|
+
if (errorInfo.details !== undefined) {
|
|
294
|
+
errorBody.details = errorInfo.details;
|
|
295
|
+
}
|
|
296
|
+
const errorResponse: StandardResponse = {
|
|
297
|
+
status: errorInfo.status,
|
|
298
|
+
headers: { "Content-Type": "application/json" },
|
|
299
|
+
body: errorBody,
|
|
300
|
+
};
|
|
301
|
+
return adapter.sendResponse(errorResponse, ctx);
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Use the shared managed runtime instead of creating a new one per request
|
|
306
|
+
if (withTracing) {
|
|
307
|
+
return managedRuntime.runPromise(
|
|
308
|
+
program.pipe(Effect.provide(NodeSdkLive)),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return managedRuntime.runPromise(program);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Create WebSocket handler using the shared managed runtime
|
|
315
|
+
const websocketHandler = await managedRuntime.runPromise(
|
|
316
|
+
adapter.webSocketHandler({
|
|
317
|
+
baseUrl,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
handler,
|
|
323
|
+
websocketHandler,
|
|
324
|
+
baseUrl,
|
|
325
|
+
dispose: () => managedRuntime.dispose(),
|
|
326
|
+
};
|
|
327
|
+
};
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { UploadistaError } from "@uploadista/core";
|
|
2
|
+
import type { Flow } from "@uploadista/core/flow";
|
|
3
|
+
import type {
|
|
4
|
+
BaseEventEmitterService,
|
|
5
|
+
BaseKvStoreService,
|
|
6
|
+
DataStoreConfig,
|
|
7
|
+
EventBroadcasterService,
|
|
8
|
+
UploadFileDataStore,
|
|
9
|
+
UploadFileKVStore,
|
|
10
|
+
} from "@uploadista/core/types";
|
|
11
|
+
import type { GenerateId } from "@uploadista/core/utils";
|
|
12
|
+
import type { Effect, Layer } from "effect";
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
import type { ServerAdapter } from "../adapter";
|
|
15
|
+
import type { AuthCacheConfig } from "../cache";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Function type for retrieving flows based on flow ID and client ID.
|
|
19
|
+
*
|
|
20
|
+
* This function is called by the core server when a flow needs to be executed.
|
|
21
|
+
* It should return an Effect that resolves to the requested Flow or fails with
|
|
22
|
+
* an UploadistaError if the flow is not found or not authorized.
|
|
23
|
+
*
|
|
24
|
+
* @param flowId - The unique identifier of the flow to retrieve
|
|
25
|
+
* @param clientId - The authenticated client ID (null if not authenticated)
|
|
26
|
+
* @returns Effect that produces the Flow or fails with an error
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const flows: FlowsFunction = (flowId, clientId) =>
|
|
31
|
+
* Effect.gen(function* () {
|
|
32
|
+
* if (flowId === "image-resize") {
|
|
33
|
+
* return imageResizeFlow;
|
|
34
|
+
* }
|
|
35
|
+
* return yield* Effect.fail(
|
|
36
|
+
* new UploadistaError({ code: "FLOW_NOT_FOUND", status: 404 })
|
|
37
|
+
* );
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export type FlowsFunction = (
|
|
42
|
+
flowId: string,
|
|
43
|
+
clientId: string | null,
|
|
44
|
+
) => Effect.Effect<
|
|
45
|
+
Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, unknown>,
|
|
46
|
+
UploadistaError,
|
|
47
|
+
unknown
|
|
48
|
+
>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for creating the unified Uploadista server.
|
|
52
|
+
*
|
|
53
|
+
* This configuration is framework-agnostic and contains all the business logic
|
|
54
|
+
* configuration. Framework-specific details are provided via the `adapter` field.
|
|
55
|
+
*
|
|
56
|
+
* @template TRequest - Framework-specific request type
|
|
57
|
+
* @template TResponse - Framework-specific response type
|
|
58
|
+
* @template TWebSocket - Framework-specific WebSocket type (optional)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { createUploadistaServer, honoAdapter } from "@uploadista/server";
|
|
63
|
+
*
|
|
64
|
+
* const config: UploadistaServerConfig<Context, Context, WSEvents> = {
|
|
65
|
+
* // Core business logic configuration
|
|
66
|
+
* flows: getFlowById,
|
|
67
|
+
* dataStore: { type: "s3", config: { bucket: "my-bucket" } },
|
|
68
|
+
* kvStore: redisKvStore,
|
|
69
|
+
* baseUrl: "/uploadista",
|
|
70
|
+
*
|
|
71
|
+
* // Framework-specific adapter
|
|
72
|
+
* adapter: honoAdapter({
|
|
73
|
+
* authMiddleware: async (c) => {
|
|
74
|
+
* // Hono-specific auth logic
|
|
75
|
+
* return { clientId: "user-123" };
|
|
76
|
+
* },
|
|
77
|
+
* }),
|
|
78
|
+
* };
|
|
79
|
+
*
|
|
80
|
+
* const server = await createUploadistaServer(config);
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export interface UploadistaServerConfig<
|
|
84
|
+
TRequest,
|
|
85
|
+
TResponse,
|
|
86
|
+
TWebSocket = unknown,
|
|
87
|
+
> {
|
|
88
|
+
/**
|
|
89
|
+
* Function for retrieving flows by ID.
|
|
90
|
+
*
|
|
91
|
+
* This function is called when a flow execution is requested.
|
|
92
|
+
* It receives the flow ID and client ID (from auth context) and should
|
|
93
|
+
* return an Effect that resolves to the Flow definition.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* flows: (flowId, clientId) => Effect.succeed(myFlows[flowId])
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
flows: FlowsFunction;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Data store configuration for file storage.
|
|
104
|
+
*
|
|
105
|
+
* Specifies where uploaded files should be stored (S3, Azure, GCS, filesystem).
|
|
106
|
+
* The core server creates the appropriate data store layer from this configuration.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* dataStore: {
|
|
111
|
+
* type: "s3",
|
|
112
|
+
* config: {
|
|
113
|
+
* bucket: "my-uploads",
|
|
114
|
+
* region: "us-east-1"
|
|
115
|
+
* }
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
dataStore: DataStoreConfig;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Key-value store layer for metadata storage.
|
|
123
|
+
*
|
|
124
|
+
* Used for storing upload metadata, flow job state, and other persistent data.
|
|
125
|
+
* Can be Redis, Cloudflare KV, in-memory, or any implementation of BaseKvStoreService.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* import { redisKvStore } from "@uploadista/kv-store-redis";
|
|
130
|
+
*
|
|
131
|
+
* kvStore: redisKvStore({ url: "redis://localhost:6379" })
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
kvStore: Layer.Layer<BaseKvStoreService>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Optional: Plugins to extend functionality.
|
|
138
|
+
*
|
|
139
|
+
* Plugins are Effect layers that add additional capabilities to the upload
|
|
140
|
+
* and flow servers. They are merged into the server layer composition.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* plugins: [imageProcessingPlugin, virusScanPlugin]
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
// biome-ignore lint/suspicious/noExplicitAny: Permissive constraint allows plugin tuples
|
|
148
|
+
plugins?: readonly Layer.Layer<any, never, never>[];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Optional: Event emitter layer for progress notifications.
|
|
152
|
+
*
|
|
153
|
+
* Used to emit upload progress, flow status updates, and other events.
|
|
154
|
+
* Defaults to in-memory event emitter if not provided.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* import { webSocketEventEmitter } from "@uploadista/event-emitter-websocket";
|
|
159
|
+
*
|
|
160
|
+
* eventEmitter: webSocketEventEmitter()
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
eventEmitter?: Layer.Layer<BaseEventEmitterService>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Optional: Event broadcaster layer for real-time updates.
|
|
167
|
+
*
|
|
168
|
+
* Used to broadcast events to multiple subscribers (e.g., WebSocket connections).
|
|
169
|
+
* Defaults to in-memory broadcaster if not provided.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* import { memoryEventBroadcaster } from "@uploadista/event-broadcaster-memory";
|
|
174
|
+
*
|
|
175
|
+
* eventBroadcaster: memoryEventBroadcaster()
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
eventBroadcaster?: Layer.Layer<EventBroadcasterService>;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Optional: Base URL path for Uploadista endpoints.
|
|
182
|
+
*
|
|
183
|
+
* All Uploadista routes will be prefixed with `{baseUrl}/api/`.
|
|
184
|
+
* For example, with baseUrl="/uploadista", routes become:
|
|
185
|
+
* - POST /uploadista/api/upload
|
|
186
|
+
* - POST /uploadista/api/flow/{flowId}/{storageId}
|
|
187
|
+
*
|
|
188
|
+
* @default "" (no prefix, routes start with /api/)
|
|
189
|
+
*/
|
|
190
|
+
baseUrl?: string;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Optional: Custom ID generator layer.
|
|
194
|
+
*
|
|
195
|
+
* Used for generating upload IDs, job IDs, and other unique identifiers.
|
|
196
|
+
* Defaults to built-in ID generator if not provided.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* import { nanoidGenerator } from "@uploadista/utils";
|
|
201
|
+
*
|
|
202
|
+
* generateId: nanoidGenerator()
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
generateId?: Layer.Layer<GenerateId>;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Optional: Enable distributed tracing with OpenTelemetry.
|
|
209
|
+
*
|
|
210
|
+
* When true, Effect programs run with OpenTelemetry tracing enabled,
|
|
211
|
+
* allowing observability into upload and flow execution.
|
|
212
|
+
*
|
|
213
|
+
* @default false
|
|
214
|
+
*/
|
|
215
|
+
withTracing?: boolean;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Optional: Metrics layer for observability.
|
|
219
|
+
*
|
|
220
|
+
* Used to collect metrics about upload/flow performance, errors, and usage.
|
|
221
|
+
* Defaults to NoOp metrics if not provided.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* import { prometheusMetrics } from "@uploadista/observability";
|
|
226
|
+
*
|
|
227
|
+
* metricsLayer: prometheusMetrics()
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
// biome-ignore lint/suspicious/noExplicitAny: MetricsService is defined in @uploadista/observability
|
|
231
|
+
metricsLayer?: Layer.Layer<any, never, never>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Optional: Buffered data store layer for performance optimization.
|
|
235
|
+
*
|
|
236
|
+
* Provides in-memory buffering for frequently accessed data,
|
|
237
|
+
* reducing latency for chunk uploads and reads.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* ```typescript
|
|
241
|
+
* import { bufferedDataStore } from "@uploadista/core/data-store";
|
|
242
|
+
*
|
|
243
|
+
* bufferedDataStore: bufferedDataStore({ maxSize: 1024 * 1024 * 10 })
|
|
244
|
+
* ```
|
|
245
|
+
*/
|
|
246
|
+
bufferedDataStore?: Layer.Layer<
|
|
247
|
+
UploadFileDataStore,
|
|
248
|
+
never,
|
|
249
|
+
UploadFileKVStore
|
|
250
|
+
>;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Framework-specific adapter.
|
|
254
|
+
*
|
|
255
|
+
* The adapter provides the bridge between framework-specific request/response
|
|
256
|
+
* types and the standard types used by the core server. Each framework
|
|
257
|
+
* (Hono, Express, Fastify) has its own adapter implementation.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```typescript
|
|
261
|
+
* import { honoAdapter } from "@uploadista/adapters-hono";
|
|
262
|
+
*
|
|
263
|
+
* adapter: honoAdapter({
|
|
264
|
+
* authMiddleware: async (c) => ({ clientId: "user-123" })
|
|
265
|
+
* })
|
|
266
|
+
* ```
|
|
267
|
+
*/
|
|
268
|
+
adapter: ServerAdapter<TRequest, TResponse, TWebSocket>;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Optional: Configuration for auth context caching.
|
|
272
|
+
*
|
|
273
|
+
* Caching allows subsequent requests (chunk uploads, flow continuations) to
|
|
274
|
+
* reuse the auth context from the initial request without re-authenticating.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```typescript
|
|
278
|
+
* authCacheConfig: {
|
|
279
|
+
* maxSize: 10000,
|
|
280
|
+
* ttl: 3600000 // 1 hour
|
|
281
|
+
* }
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
authCacheConfig?: AuthCacheConfig;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Return type from createUploadistaServer.
|
|
289
|
+
*
|
|
290
|
+
* Contains the handler function and exposed server layers for framework-specific routing.
|
|
291
|
+
*
|
|
292
|
+
* @template TRequest - Framework-specific request type
|
|
293
|
+
* @template TResponse - Framework-specific response type
|
|
294
|
+
* @template TWebSocket - Framework-specific WebSocket type (optional)
|
|
295
|
+
*/
|
|
296
|
+
export interface UploadistaServer<
|
|
297
|
+
TRequest,
|
|
298
|
+
TResponse,
|
|
299
|
+
TWebSocketHandler = unknown,
|
|
300
|
+
> {
|
|
301
|
+
/**
|
|
302
|
+
* Main request handler that processes HTTP requests through the adapter.
|
|
303
|
+
*/
|
|
304
|
+
handler: (req: TRequest) => Promise<TResponse>;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Optional WebSocket handler if the adapter supports WebSocket connections.
|
|
308
|
+
*/
|
|
309
|
+
websocketHandler: TWebSocketHandler;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Base URL path for Uploadista endpoints.
|
|
313
|
+
*/
|
|
314
|
+
baseUrl: string;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Dispose function for graceful shutdown.
|
|
318
|
+
* Cleans up all resources associated with the managed runtime.
|
|
319
|
+
* Should be called when the server is shutting down.
|
|
320
|
+
*/
|
|
321
|
+
dispose: () => Promise<void>;
|
|
322
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { FlowServerShape } from "@uploadista/core/flow";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import type { WebSocketConnection } from "../websocket-routes";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles subscription to flow events
|
|
7
|
+
* Subscribes the WebSocket connection to receive real-time flow execution events
|
|
8
|
+
*/
|
|
9
|
+
export const handleSubscribeToFlowEvents = (
|
|
10
|
+
flowServer: FlowServerShape,
|
|
11
|
+
jobId: string | undefined,
|
|
12
|
+
connection: WebSocketConnection,
|
|
13
|
+
) => {
|
|
14
|
+
return Effect.gen(function* () {
|
|
15
|
+
if (!jobId) {
|
|
16
|
+
yield* Effect.sync(() => {
|
|
17
|
+
connection.send(
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
type: "error",
|
|
20
|
+
message: "Job ID is required for flow event subscription",
|
|
21
|
+
code: "MISSING_JOB_ID",
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
yield* flowServer.subscribeToFlowEvents(jobId, connection);
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handles unsubscription from flow events
|
|
34
|
+
* Removes the WebSocket connection from receiving flow events
|
|
35
|
+
*/
|
|
36
|
+
export const handleUnsubscribeFromFlowEvents = (
|
|
37
|
+
flowServer: FlowServerShape,
|
|
38
|
+
jobId: string | undefined,
|
|
39
|
+
) => {
|
|
40
|
+
return Effect.gen(function* () {
|
|
41
|
+
if (!jobId) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
yield* flowServer.unsubscribeFromFlowEvents(jobId);
|
|
46
|
+
});
|
|
47
|
+
};
|