@uploadista/server 0.0.9 → 0.0.11

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.
@@ -0,0 +1,369 @@
1
+ # Plugin Typing System
2
+
3
+ ## Overview
4
+
5
+ The Uploadista server has a sophisticated typing system that allows flows to depend on plugins while maintaining type safety. This document explains how the typing works and how to use it effectively.
6
+
7
+ ## Core Concepts
8
+
9
+ ### Flow Requirements
10
+
11
+ Flows can have dependencies on services (like `ImagePlugin`) that are provided through Effect's dependency injection system. The `Flow` type has a third generic parameter `TRequirements` that captures these dependencies:
12
+
13
+ ```typescript
14
+ type Flow<
15
+ TFlowInputSchema extends z.ZodSchema<any>,
16
+ TFlowOutputSchema extends z.ZodSchema<any>,
17
+ TRequirements // Services that flow nodes need
18
+ >
19
+ ```
20
+
21
+ ### Plugin Layers
22
+
23
+ Plugins are Effect layers that provide services to flows. They are typed as:
24
+
25
+ ```typescript
26
+ Layer.Layer<TOutput, TError, TRequirements>
27
+ ```
28
+
29
+ Where:
30
+ - `TOutput`: The service(s) this plugin provides
31
+ - `TError`: Errors the plugin can produce (usually `never`)
32
+ - `TRequirements`: Services this plugin needs (can be `never` or other services)
33
+
34
+ ## Type Parameters
35
+
36
+ The `createUploadistaServer` function has these key type parameters:
37
+
38
+ ```typescript
39
+ createUploadistaServer<
40
+ TContext, // Framework-specific context
41
+ TResponse, // Framework-specific response
42
+ TWebSocket, // WebSocket handler type
43
+ TFlows, // Flow function type
44
+ TPlugins // Plugin layers tuple
45
+ >
46
+ ```
47
+
48
+ ### TFlows
49
+
50
+ The flows function type that returns Effects producing Flows:
51
+
52
+ ```typescript
53
+ TFlows extends (
54
+ flowId: string,
55
+ clientId: string | null,
56
+ ) => Effect.Effect<
57
+ Flow<z.ZodSchema<unknown>, z.ZodSchema<unknown>, any>,
58
+ UploadistaError,
59
+ any // Flow requirements - can be ImagePlugin, etc.
60
+ >
61
+ ```
62
+
63
+ The `any` here allows flows to have any requirements, which will be satisfied by plugins.
64
+
65
+ ### TPlugins
66
+
67
+ A readonly tuple of plugin layers:
68
+
69
+ ```typescript
70
+ TPlugins extends readonly Layer.Layer<any, never, any>[]
71
+ ```
72
+
73
+ The type parameters are:
74
+ - First `any`: Plugins can provide any services
75
+ - `never`: Plugins should not error
76
+ - Second `any`: Plugins can have requirements (satisfied by other plugins or at runtime)
77
+
78
+ ## Usage Examples
79
+
80
+ ### Basic Flow Without Plugins
81
+
82
+ ```typescript
83
+ const server = await createUploadistaServer({
84
+ flows: (flowId) => Effect.succeed(basicFlow),
85
+ dataStore: { type: "s3", config: { bucket: "uploads" } },
86
+ kvStore: redisKvStore,
87
+ adapter: honoAdapter({ /* ... */ })
88
+ });
89
+ ```
90
+
91
+ ### Flow With Image Plugin
92
+
93
+ ```typescript
94
+ import { sharpImagePlugin } from "@uploadista/flow-images-sharp";
95
+
96
+ // Create a flow that uses ImagePlugin
97
+ const imageFlow = Effect.gen(function* () {
98
+ const imageService = yield* ImagePlugin;
99
+
100
+ return createFlow({
101
+ id: "resize-flow",
102
+ nodes: [
103
+ yield* createResizeNode("resize", {
104
+ width: 800,
105
+ height: 600,
106
+ fit: "cover"
107
+ })
108
+ ],
109
+ // ... edges, schemas
110
+ });
111
+ });
112
+
113
+ // Create server with plugin
114
+ const server = await createUploadistaServer({
115
+ flows: (flowId) => {
116
+ if (flowId === "resize") return imageFlow;
117
+ return Effect.fail(new UploadistaError({
118
+ code: "FLOW_NOT_FOUND"
119
+ }));
120
+ },
121
+ plugins: [sharpImagePlugin], // Provides ImagePlugin
122
+ dataStore: { type: "s3", config: { bucket: "uploads" } },
123
+ kvStore: redisKvStore,
124
+ adapter: honoAdapter({ /* ... */ })
125
+ });
126
+ ```
127
+
128
+ ### Multiple Plugins
129
+
130
+ ```typescript
131
+ const server = await createUploadistaServer({
132
+ flows: getFlowById,
133
+ plugins: [
134
+ sharpImagePlugin, // Provides ImagePlugin
135
+ virusScanPlugin, // Provides VirusScanService
136
+ customProcessingPlugin // Provides CustomProcessingService
137
+ ] as const, // 'as const' preserves tuple type
138
+ dataStore: { type: "s3", config: { bucket: "uploads" } },
139
+ kvStore: redisKvStore,
140
+ adapter: honoAdapter({ /* ... */ })
141
+ });
142
+ ```
143
+
144
+ ## Implementation Details
145
+
146
+ ### Layer Composition
147
+
148
+ The server merges all layers together:
149
+
150
+ ```typescript
151
+ const serverLayer = Layer.mergeAll(
152
+ uploadServerLayer, // Provides UploadServer
153
+ flowServerLayer, // Provides FlowServer
154
+ metricsLayer, // Provides MetricsService
155
+ authCacheLayer, // Provides AuthCacheService
156
+ ...plugins // Provides plugin services (ImagePlugin, etc.)
157
+ );
158
+ ```
159
+
160
+ ### Type Assertions
161
+
162
+ Due to the dynamic nature of plugins, we use type assertions to bridge Effect's strict typing with the plugin system:
163
+
164
+ ```typescript
165
+ const serverLayer = serverLayerRaw as unknown as Layer.Layer<any, never, any>;
166
+ const managedRuntime = ManagedRuntime.make(serverLayer as any);
167
+ ```
168
+
169
+ These assertions are necessary because:
170
+ 1. Plugin requirements are determined at runtime based on which plugins are provided
171
+ 2. TypeScript cannot statically verify all possible plugin combinations
172
+ 3. The Effect runtime handles requirement resolution dynamically
173
+
174
+ ### Request Context Layer
175
+
176
+ For each request, plugin services are merged with request-specific context:
177
+
178
+ ```typescript
179
+ const requestContextLayer = Layer.mergeAll(
180
+ authContextLayer, // Auth for this request
181
+ authCacheLayer, // Shared auth cache
182
+ effectiveMetricsLayer, // Metrics service
183
+ ...plugins, // All plugin services
184
+ ...waitUntilLayers // Cloudflare waitUntil if available
185
+ );
186
+
187
+ // Flow execution with all dependencies
188
+ const response = yield* handleUploadistaRequest(uploadistaRequest)
189
+ .pipe(Effect.provide(requestContextLayer));
190
+ ```
191
+
192
+ This ensures that:
193
+ - Flow nodes have access to all plugin services (ImagePlugin, etc.)
194
+ - Each request has isolated auth context
195
+ - Metrics and caching are shared across requests
196
+
197
+ ## Best Practices
198
+
199
+ ### 1. Use `as const` for Plugin Arrays
200
+
201
+ ```typescript
202
+ const plugins = [
203
+ imagePlugin,
204
+ scanPlugin
205
+ ] as const;
206
+ ```
207
+
208
+ This preserves the tuple type and enables better type inference.
209
+
210
+ ### 2. Type Flow Requirements Explicitly
211
+
212
+ When creating flows, explicitly type the requirements:
213
+
214
+ ```typescript
215
+ const myFlow: Effect.Effect<
216
+ Flow<InputSchema, OutputSchema, ImagePlugin>,
217
+ UploadistaError,
218
+ never
219
+ > = Effect.gen(function* () {
220
+ const imageService = yield* ImagePlugin;
221
+ // ...
222
+ });
223
+ ```
224
+
225
+ ### 3. Document Plugin Dependencies
226
+
227
+ Always document which plugins a flow requires:
228
+
229
+ ```typescript
230
+ /**
231
+ * Image processing flow
232
+ *
233
+ * @requires ImagePlugin - For resize and optimize operations
234
+ * @requires MetricsService - For performance tracking
235
+ */
236
+ export const imageProcessingFlow = /* ... */;
237
+ ```
238
+
239
+ ### 4. Provide Plugin Implementations
240
+
241
+ When creating a server, ensure all required plugins are provided:
242
+
243
+ ```typescript
244
+ // ❌ Bad: Flow requires ImagePlugin but none provided
245
+ createUploadistaServer({
246
+ flows: flowThatNeedsImagePlugin,
247
+ plugins: [] // Missing ImagePlugin!
248
+ });
249
+
250
+ // ✅ Good: All requirements satisfied
251
+ createUploadistaServer({
252
+ flows: flowThatNeedsImagePlugin,
253
+ plugins: [sharpImagePlugin] // Provides ImagePlugin
254
+ });
255
+ ```
256
+
257
+ ## Advanced: Custom Plugins
258
+
259
+ ### Creating a Plugin
260
+
261
+ ```typescript
262
+ import { Context, Layer, Effect } from "effect";
263
+
264
+ // 1. Define the service interface
265
+ export interface CustomService {
266
+ process: (data: Uint8Array) => Effect.Effect<Uint8Array, Error>;
267
+ }
268
+
269
+ // 2. Create the service tag
270
+ export class CustomService extends Context.Tag("CustomService")<
271
+ CustomService,
272
+ CustomService
273
+ >() {}
274
+
275
+ // 3. Create the implementation layer
276
+ export const customServiceLive = Layer.succeed(
277
+ CustomService,
278
+ {
279
+ process: (data) => Effect.sync(() => {
280
+ // Your processing logic
281
+ return data;
282
+ })
283
+ }
284
+ );
285
+ ```
286
+
287
+ ### Using Custom Plugin in Flow
288
+
289
+ ```typescript
290
+ const flowWithCustomPlugin = Effect.gen(function* () {
291
+ const customService = yield* CustomService;
292
+
293
+ return createFlow({
294
+ nodes: [
295
+ yield* createTransformNode({
296
+ id: "custom",
297
+ name: "Custom Processing",
298
+ transform: (input) => customService.process(input)
299
+ })
300
+ ],
301
+ // ...
302
+ });
303
+ });
304
+ ```
305
+
306
+ ### Adding to Server
307
+
308
+ ```typescript
309
+ const server = await createUploadistaServer({
310
+ flows: () => flowWithCustomPlugin,
311
+ plugins: [customServiceLive],
312
+ // ...
313
+ });
314
+ ```
315
+
316
+ ## Troubleshooting
317
+
318
+ ### Flow Requirements Not Satisfied
319
+
320
+ **Error**: "Service not found" at runtime
321
+
322
+ **Solution**: Ensure the plugin providing the required service is in the `plugins` array
323
+
324
+ ```typescript
325
+ // Add the missing plugin
326
+ plugins: [sharpImagePlugin] // Provides ImagePlugin
327
+ ```
328
+
329
+ ### Type Errors with Plugins
330
+
331
+ **Error**: Type mismatch when passing plugins
332
+
333
+ **Solution**: Use `as const` and ensure plugin types match `Layer.Layer<any, never, any>`
334
+
335
+ ```typescript
336
+ const plugins = [myPlugin] as const;
337
+ ```
338
+
339
+ ### Runtime Layer Composition Errors
340
+
341
+ **Error**: Layer requirements not satisfied
342
+
343
+ **Solution**: Check that plugin dependencies form a valid dependency graph (no circular dependencies)
344
+
345
+ ```typescript
346
+ // ❌ Bad: Circular dependency
347
+ const pluginA = Layer.effect(ServiceA,
348
+ Effect.gen(function* () {
349
+ yield* ServiceB; // Depends on B
350
+ return { /* ... */ };
351
+ })
352
+ );
353
+
354
+ const pluginB = Layer.effect(ServiceB,
355
+ Effect.gen(function* () {
356
+ yield* ServiceA; // Depends on A - circular!
357
+ return { /* ... */ };
358
+ })
359
+ );
360
+
361
+ // ✅ Good: Linear dependency
362
+ const pluginA = Layer.succeed(ServiceA, { /* ... */ });
363
+ const pluginB = Layer.effect(ServiceB,
364
+ Effect.gen(function* () {
365
+ yield* ServiceA; // Only B depends on A
366
+ return { /* ... */ };
367
+ })
368
+ );
369
+ ```