@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/auth/index.d.mts +2 -0
  3. package/dist/auth/index.mjs +1 -0
  4. package/dist/{auth-C77S4vQd.js → auth-BqArZeGK.mjs} +1 -1
  5. package/dist/auth-BqArZeGK.mjs.map +1 -0
  6. package/dist/{index-CvDNB1lJ.d.ts → index--Lny6VJP.d.mts} +1 -1
  7. package/dist/index--Lny6VJP.d.mts.map +1 -0
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.d.cts +735 -12
  10. package/dist/index.d.cts.map +1 -1
  11. package/dist/index.d.mts +1343 -0
  12. package/dist/index.d.mts.map +1 -0
  13. package/dist/index.mjs +2 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +10 -7
  16. package/src/adapter/index.ts +10 -0
  17. package/src/adapter/types.ts +229 -0
  18. package/src/core/http-handlers/flow-http-handlers.ts +245 -0
  19. package/src/core/http-handlers/http-handlers.ts +72 -0
  20. package/src/core/http-handlers/upload-http-handlers.ts +168 -0
  21. package/src/core/index.ts +12 -0
  22. package/src/core/routes.ts +188 -0
  23. package/src/core/server.ts +327 -0
  24. package/src/core/types.ts +322 -0
  25. package/src/core/websocket-handlers/flow-websocket-handlers.ts +47 -0
  26. package/src/core/websocket-handlers/upload-websocket-handlers.ts +47 -0
  27. package/src/core/websocket-handlers/websocket-handlers.ts +151 -0
  28. package/src/core/websocket-routes.ts +136 -0
  29. package/src/index.ts +2 -0
  30. package/dist/auth/index.d.ts +0 -2
  31. package/dist/auth/index.js +0 -1
  32. package/dist/auth-C77S4vQd.js.map +0 -1
  33. package/dist/index-CvDNB1lJ.d.ts.map +0 -1
  34. package/dist/index.d.ts +0 -620
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/index.js +0 -2
  37. 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
+ };