@effect/platform 0.28.3 → 0.29.0

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 (38) hide show
  1. package/dist/cjs/Http/FormData.js +21 -26
  2. package/dist/cjs/Http/FormData.js.map +1 -1
  3. package/dist/cjs/Http/ServerRequest.js +3 -3
  4. package/dist/cjs/Http/ServerRequest.js.map +1 -1
  5. package/dist/cjs/Terminal.js.map +1 -1
  6. package/dist/cjs/internal/http/formData.js +212 -31
  7. package/dist/cjs/internal/http/formData.js.map +1 -1
  8. package/dist/cjs/internal/http/serverRequest.js +4 -4
  9. package/dist/cjs/internal/http/serverRequest.js.map +1 -1
  10. package/dist/cjs/internal/worker.js +6 -5
  11. package/dist/cjs/internal/worker.js.map +1 -1
  12. package/dist/dts/Http/FormData.d.ts +46 -35
  13. package/dist/dts/Http/FormData.d.ts.map +1 -1
  14. package/dist/dts/Http/ServerRequest.d.ts +3 -3
  15. package/dist/dts/Http/ServerRequest.d.ts.map +1 -1
  16. package/dist/dts/Terminal.d.ts +4 -0
  17. package/dist/dts/Terminal.d.ts.map +1 -1
  18. package/dist/dts/WorkerRunner.d.ts +1 -1
  19. package/dist/dts/WorkerRunner.d.ts.map +1 -1
  20. package/dist/esm/Http/FormData.js +20 -25
  21. package/dist/esm/Http/FormData.js.map +1 -1
  22. package/dist/esm/Http/ServerRequest.js +1 -1
  23. package/dist/esm/Http/ServerRequest.js.map +1 -1
  24. package/dist/esm/Terminal.js.map +1 -1
  25. package/dist/esm/internal/http/formData.js +207 -29
  26. package/dist/esm/internal/http/formData.js.map +1 -1
  27. package/dist/esm/internal/http/serverRequest.js +3 -3
  28. package/dist/esm/internal/http/serverRequest.js.map +1 -1
  29. package/dist/esm/internal/worker.js +2 -1
  30. package/dist/esm/internal/worker.js.map +1 -1
  31. package/package.json +2 -1
  32. package/src/Http/FormData.ts +62 -41
  33. package/src/Http/ServerRequest.ts +9 -5
  34. package/src/Terminal.ts +4 -0
  35. package/src/WorkerRunner.ts +1 -1
  36. package/src/internal/http/formData.ts +320 -62
  37. package/src/internal/http/serverRequest.ts +4 -10
  38. package/src/internal/worker.ts +2 -1
package/src/Terminal.ts CHANGED
@@ -16,6 +16,10 @@ import * as InternalTerminal from "./internal/terminal.js"
16
16
  * @category models
17
17
  */
18
18
  export interface Terminal {
19
+ /**
20
+ * The number of columns available on the platform's terminal interface.
21
+ */
22
+ readonly columns: number
19
23
  /**
20
24
  * Reads a single input event from the default standard input.
21
25
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import type { Effect } from "effect"
5
4
  import type * as Context from "effect/Context"
5
+ import type * as Effect from "effect/Effect"
6
6
  import type * as Fiber from "effect/Fiber"
7
7
  import type * as Queue from "effect/Queue"
8
8
  import type * as Scope from "effect/Scope"
@@ -1,16 +1,24 @@
1
1
  import type * as ParseResult from "@effect/schema/ParseResult"
2
2
  import * as Schema from "@effect/schema/Schema"
3
+ import * as Cause from "effect/Cause"
4
+ import * as Channel from "effect/Channel"
5
+ import type * as AsyncInput from "effect/ChannelSingleProducerAsyncInput"
3
6
  import * as Chunk from "effect/Chunk"
4
7
  import * as Data from "effect/Data"
5
8
  import * as Effect from "effect/Effect"
6
9
  import * as FiberRef from "effect/FiberRef"
7
- import { dual, pipe } from "effect/Function"
10
+ import { dual, flow, pipe } from "effect/Function"
8
11
  import { globalValue } from "effect/GlobalValue"
9
12
  import * as Option from "effect/Option"
10
13
  import * as Predicate from "effect/Predicate"
11
- import * as ReadonlyArray from "effect/ReadonlyArray"
14
+ import * as Queue from "effect/Queue"
15
+ import type * as Scope from "effect/Scope"
16
+ import * as Stream from "effect/Stream"
17
+ import * as MP from "multipasta"
12
18
  import * as FileSystem from "../../FileSystem.js"
13
19
  import type * as FormData from "../../Http/FormData.js"
20
+ import * as IncomingMessage from "../../Http/IncomingMessage.js"
21
+ import * as Path from "../../Path.js"
14
22
 
15
23
  /** @internal */
16
24
  export const TypeId: FormData.TypeId = Symbol.for("@effect/platform/Http/FormData") as FormData.TypeId
@@ -29,6 +37,10 @@ export const FormDataError = (reason: FormData.FormDataError["reason"], error: u
29
37
  error
30
38
  })
31
39
 
40
+ /** @internal */
41
+ export const isField = (u: unknown): u is FormData.Field =>
42
+ Predicate.hasProperty(u, TypeId) && Predicate.isTagged(u, "Field")
43
+
32
44
  /** @internal */
33
45
  export const maxParts: FiberRef.FiberRef<Option.Option<number>> = globalValue(
34
46
  "@effect/platform/Http/FormData/maxParts",
@@ -53,30 +65,6 @@ export const withMaxFieldSize = dual<
53
65
  <R, E, A>(effect: Effect.Effect<R, E, A>, size: FileSystem.SizeInput) => Effect.Effect<R, E, A>
54
66
  >(2, (effect, size) => Effect.locally(effect, maxFieldSize, FileSystem.Size(size)))
55
67
 
56
- /** @internal */
57
- export const maxFields: FiberRef.FiberRef<Option.Option<number>> = globalValue(
58
- "@effect/platform/Http/FormData/maxFields",
59
- () => FiberRef.unsafeMake(Option.none<number>())
60
- )
61
-
62
- /** @internal */
63
- export const withMaxFields = dual<
64
- (count: Option.Option<number>) => <R, E, A>(effect: Effect.Effect<R, E, A>) => Effect.Effect<R, E, A>,
65
- <R, E, A>(effect: Effect.Effect<R, E, A>, count: Option.Option<number>) => Effect.Effect<R, E, A>
66
- >(2, (effect, count) => Effect.locally(effect, maxFields, count))
67
-
68
- /** @internal */
69
- export const maxFiles: FiberRef.FiberRef<Option.Option<number>> = globalValue(
70
- "@effect/platform/Http/FormData/maxFiles",
71
- () => FiberRef.unsafeMake(Option.none<number>())
72
- )
73
-
74
- /** @internal */
75
- export const withMaxFiles = dual<
76
- (count: Option.Option<number>) => <R, E, A>(effect: Effect.Effect<R, E, A>) => Effect.Effect<R, E, A>,
77
- <R, E, A>(effect: Effect.Effect<R, E, A>, count: Option.Option<number>) => Effect.Effect<R, E, A>
78
- >(2, (effect, count) => Effect.locally(effect, maxFiles, count))
79
-
80
68
  /** @internal */
81
69
  export const maxFileSize: FiberRef.FiberRef<Option.Option<FileSystem.Size>> = globalValue(
82
70
  "@effect/platform/Http/FormData/maxFileSize",
@@ -102,49 +90,32 @@ export const withFieldMimeTypes = dual<
102
90
  >(2, (effect, mimeTypes) => Effect.locally(effect, fieldMimeTypes, Chunk.fromIterable(mimeTypes)))
103
91
 
104
92
  /** @internal */
105
- export const toRecord = (formData: globalThis.FormData): Record<string, Array<globalThis.File> | string> =>
106
- ReadonlyArray.reduce(
107
- formData.entries(),
108
- {} as Record<string, Array<globalThis.File> | string>,
109
- (acc, [key, value]) => {
110
- if (Predicate.isString(value)) {
111
- acc[key] = value
112
- } else {
113
- const existing = acc[key]
114
- if (Array.isArray(existing)) {
115
- existing.push(value)
116
- } else {
117
- acc[key] = [value]
118
- }
119
- }
120
- return acc
121
- }
122
- )
123
- /** @internal */
124
- export const filesSchema: Schema.Schema<ReadonlyArray<File>, ReadonlyArray<File>> = Schema.array(
125
- pipe(
126
- Schema.instanceOf(Blob),
127
- Schema.filter(
128
- (blob): blob is File => "name" in blob
93
+ export const filesSchema: Schema.Schema<ReadonlyArray<FormData.PersistedFile>, ReadonlyArray<FormData.PersistedFile>> =
94
+ Schema
95
+ .array(
96
+ pipe(
97
+ Schema.object,
98
+ Schema.filter(
99
+ (file): file is FormData.PersistedFile => TypeId in file && "_tag" in file && file._tag === "PersistedFile"
100
+ )
101
+ ) as any as Schema.Schema<FormData.PersistedFile, FormData.PersistedFile>
129
102
  )
130
- ) as any as Schema.Schema<File, File>
131
- )
132
103
 
133
104
  /** @internal */
134
- export const schemaRecord = <I extends Readonly<Record<string, string | ReadonlyArray<globalThis.File>>>, A>(
105
+ export const schemaPersisted = <I extends FormData.PersistedFormData, A>(
135
106
  schema: Schema.Schema<I, A>
136
107
  ) => {
137
108
  const parse = Schema.parse(schema)
138
- return (formData: globalThis.FormData) => parse(toRecord(formData))
109
+ return (formData: FormData.PersistedFormData) => parse(formData)
139
110
  }
140
111
 
141
112
  /** @internal */
142
113
  export const schemaJson = <I, A>(schema: Schema.Schema<I, A>): {
143
114
  (
144
115
  field: string
145
- ): (formData: globalThis.FormData) => Effect.Effect<never, FormData.FormDataError | ParseResult.ParseError, A>
116
+ ): (formData: FormData.PersistedFormData) => Effect.Effect<never, FormData.FormDataError | ParseResult.ParseError, A>
146
117
  (
147
- formData: globalThis.FormData,
118
+ formData: FormData.PersistedFormData,
148
119
  field: string
149
120
  ): Effect.Effect<never, FormData.FormDataError | ParseResult.ParseError, A>
150
121
  } => {
@@ -152,22 +123,309 @@ export const schemaJson = <I, A>(schema: Schema.Schema<I, A>): {
152
123
  return dual<
153
124
  (
154
125
  field: string
155
- ) => (formData: globalThis.FormData) => Effect.Effect<never, FormData.FormDataError | ParseResult.ParseError, A>,
126
+ ) => (
127
+ formData: FormData.PersistedFormData
128
+ ) => Effect.Effect<never, FormData.FormDataError | ParseResult.ParseError, A>,
156
129
  (
157
- formData: globalThis.FormData,
130
+ formData: FormData.PersistedFormData,
158
131
  field: string
159
132
  ) => Effect.Effect<never, FormData.FormDataError | ParseResult.ParseError, A>
160
133
  >(2, (formData, field) =>
161
134
  pipe(
162
- Effect.succeed(formData.get(field)),
135
+ Effect.succeed(formData[field]),
163
136
  Effect.filterOrFail(
164
- (field) => Predicate.isString(field),
165
- () => FormDataError("Parse", `schemaJson: field was not a string`)
137
+ isField,
138
+ () => FormDataError("Parse", `schemaJson: was not a field`)
166
139
  ),
167
140
  Effect.tryMap({
168
- try: (field) => JSON.parse(field as string),
141
+ try: (field) => JSON.parse(field.value),
169
142
  catch: (error) => FormDataError("Parse", `schemaJson: field was not valid json: ${error}`)
170
143
  }),
171
144
  Effect.flatMap(parse)
172
145
  ))
173
146
  }
147
+
148
+ /** @internal */
149
+ export const makeConfig = (
150
+ headers: Record<string, string>
151
+ ): Effect.Effect<never, never, MP.BaseConfig> =>
152
+ Effect.map(
153
+ Effect.all({
154
+ maxParts: Effect.map(FiberRef.get(maxParts), Option.getOrUndefined),
155
+ maxFieldSize: Effect.map(FiberRef.get(maxFieldSize), Number),
156
+ maxPartSize: Effect.map(FiberRef.get(maxFileSize), flow(Option.map(Number), Option.getOrUndefined)),
157
+ maxTotalSize: Effect.map(
158
+ FiberRef.get(IncomingMessage.maxBodySize),
159
+ flow(Option.map(Number), Option.getOrUndefined)
160
+ ),
161
+ isFile: Effect.map(FiberRef.get(fieldMimeTypes), (mimeTypes) => {
162
+ if (mimeTypes.length === 0) {
163
+ return undefined
164
+ }
165
+ return (info: MP.PartInfo): boolean =>
166
+ Chunk.some(mimeTypes, (_) => info.contentType.includes(_)) || MP.defaultIsFile(info)
167
+ })
168
+ }),
169
+ (_) => ({ ..._, headers })
170
+ )
171
+
172
+ /** @internal */
173
+ export const makeChannel = <IE>(
174
+ headers: Record<string, string>,
175
+ bufferSize = 16
176
+ ): Channel.Channel<
177
+ never,
178
+ IE,
179
+ Chunk.Chunk<Uint8Array>,
180
+ unknown,
181
+ FormData.FormDataError | IE,
182
+ Chunk.Chunk<FormData.Part>,
183
+ unknown
184
+ > =>
185
+ Channel.acquireUseRelease(
186
+ Effect.all([
187
+ makeConfig(headers),
188
+ Queue.bounded<Chunk.Chunk<Uint8Array> | null>(bufferSize)
189
+ ]),
190
+ ([config, queue]) => makeFromQueue(config, queue),
191
+ ([, queue]) => Queue.shutdown(queue)
192
+ )
193
+
194
+ const makeFromQueue = <IE>(
195
+ config: MP.BaseConfig,
196
+ queue: Queue.Queue<Chunk.Chunk<Uint8Array> | null>
197
+ ): Channel.Channel<
198
+ never,
199
+ IE,
200
+ Chunk.Chunk<Uint8Array>,
201
+ unknown,
202
+ IE | FormData.FormDataError,
203
+ Chunk.Chunk<FormData.Part>,
204
+ unknown
205
+ > =>
206
+ Channel.suspend(() => {
207
+ let error = Option.none<Cause.Cause<IE | FormData.FormDataError>>()
208
+ let partsBuffer: Array<FormData.Part> = []
209
+ let partsFinished = false
210
+
211
+ const input: AsyncInput.AsyncInputProducer<IE, Chunk.Chunk<Uint8Array>, unknown> = {
212
+ awaitRead: () => Effect.unit,
213
+ emit(element) {
214
+ return Queue.offer(queue, element)
215
+ },
216
+ error(cause) {
217
+ error = Option.some(cause)
218
+ return Queue.offer(queue, null)
219
+ },
220
+ done(_value) {
221
+ return Queue.offer(queue, null)
222
+ }
223
+ }
224
+
225
+ const parser = MP.make({
226
+ ...config,
227
+ onField(info, value) {
228
+ partsBuffer.push(new FieldImpl(info.name, info.contentType, MP.decodeField(info, value)))
229
+ },
230
+ onFile(info) {
231
+ let chunks: Array<Uint8Array> = []
232
+ let finished = false
233
+ const take: Channel.Channel<never, unknown, unknown, unknown, never, Chunk.Chunk<Uint8Array>, void> = Channel
234
+ .suspend(() => {
235
+ if (finished) {
236
+ return Channel.unit
237
+ } else if (chunks.length === 0) {
238
+ return Channel.zipRight(pump, take)
239
+ }
240
+ const chunk = Chunk.unsafeFromArray(chunks)
241
+ chunks = []
242
+ return Channel.zipRight(
243
+ Channel.write(chunk),
244
+ Channel.zipRight(pump, take)
245
+ )
246
+ })
247
+ partsBuffer.push(new FileImpl(info, take))
248
+ return function(chunk) {
249
+ if (chunk === null) {
250
+ finished = true
251
+ } else {
252
+ chunks.push(chunk)
253
+ }
254
+ }
255
+ },
256
+ onError(error_) {
257
+ error = Option.some(Cause.fail(convertError(error_)))
258
+ },
259
+ onDone() {
260
+ partsFinished = true
261
+ }
262
+ })
263
+
264
+ const pump = Channel.flatMap(
265
+ Queue.take(queue),
266
+ (chunk) =>
267
+ Channel.sync(() => {
268
+ if (chunk === null) {
269
+ parser.end()
270
+ } else {
271
+ Chunk.forEach(chunk, function(buf) {
272
+ parser.write(buf)
273
+ })
274
+ }
275
+ })
276
+ )
277
+
278
+ const takeParts = Channel.zipRight(
279
+ pump,
280
+ Channel.suspend(() => {
281
+ if (partsBuffer.length === 0) {
282
+ return Channel.unit
283
+ }
284
+ const parts = Chunk.unsafeFromArray(partsBuffer)
285
+ partsBuffer = []
286
+ return Channel.write(parts)
287
+ })
288
+ )
289
+
290
+ const partsChannel: Channel.Channel<
291
+ never,
292
+ unknown,
293
+ unknown,
294
+ unknown,
295
+ IE | FormData.FormDataError,
296
+ Chunk.Chunk<FormData.Part>,
297
+ void
298
+ > = Channel.suspend(() => {
299
+ if (error._tag === "Some") {
300
+ return Channel.failCause(error.value)
301
+ } else if (partsFinished) {
302
+ return Channel.unit
303
+ }
304
+ return Channel.zipRight(takeParts, partsChannel)
305
+ })
306
+
307
+ return Channel.embedInput(partsChannel, input)
308
+ })
309
+
310
+ function convertError(error: MP.MultipartError): FormData.FormDataError {
311
+ switch (error._tag) {
312
+ case "ReachedLimit": {
313
+ switch (error.limit) {
314
+ case "MaxParts": {
315
+ return FormDataError("TooManyParts", error)
316
+ }
317
+ case "MaxFieldSize": {
318
+ return FormDataError("FieldTooLarge", error)
319
+ }
320
+ case "MaxPartSize": {
321
+ return FormDataError("FileTooLarge", error)
322
+ }
323
+ case "MaxTotalSize": {
324
+ return FormDataError("BodyTooLarge", error)
325
+ }
326
+ }
327
+ }
328
+ default: {
329
+ return FormDataError("Parse", error)
330
+ }
331
+ }
332
+ }
333
+
334
+ class FieldImpl implements FormData.Field {
335
+ readonly [TypeId]: FormData.TypeId
336
+ readonly _tag = "Field"
337
+
338
+ constructor(
339
+ readonly key: string,
340
+ readonly contentType: string,
341
+ readonly value: string
342
+ ) {
343
+ this[TypeId] = TypeId
344
+ }
345
+ }
346
+
347
+ class FileImpl implements FormData.File {
348
+ readonly _tag = "File"
349
+ readonly [TypeId]: FormData.TypeId
350
+ readonly key: string
351
+ readonly name: string
352
+ readonly contentType: string
353
+ readonly content: Stream.Stream<never, FormData.FormDataError, Uint8Array>
354
+
355
+ constructor(
356
+ info: MP.PartInfo,
357
+ channel: Channel.Channel<never, unknown, unknown, unknown, never, Chunk.Chunk<Uint8Array>, void>
358
+ ) {
359
+ this[TypeId] = TypeId
360
+ this.key = info.name
361
+ this.name = info.filename ?? info.name
362
+ this.contentType = info.contentType
363
+ this.content = Stream.fromChannel(channel)
364
+ }
365
+ }
366
+
367
+ const defaultWriteFile = (path: string, file: FormData.File) =>
368
+ Effect.flatMap(
369
+ FileSystem.FileSystem,
370
+ (fs) =>
371
+ Effect.mapError(
372
+ Stream.run(file.content, fs.sink(path)),
373
+ (error) => FormDataError("InternalError", error)
374
+ )
375
+ )
376
+
377
+ /** @internal */
378
+ export const formData = (
379
+ stream: Stream.Stream<never, FormData.FormDataError, FormData.Part>,
380
+ writeFile = defaultWriteFile
381
+ ): Effect.Effect<FileSystem.FileSystem | Path.Path | Scope.Scope, FormData.FormDataError, FormData.PersistedFormData> =>
382
+ pipe(
383
+ Effect.Do,
384
+ Effect.bind("fs", () => FileSystem.FileSystem),
385
+ Effect.bind("path", () => Path.Path),
386
+ Effect.bind("dir", ({ fs }) => fs.makeTempDirectoryScoped()),
387
+ Effect.flatMap(({ dir, path: path_ }) =>
388
+ Stream.runFoldEffect(
389
+ stream,
390
+ Object.create(null) as Record<string, Array<FormData.PersistedFile> | string>,
391
+ (formData, part) => {
392
+ if (part._tag === "Field") {
393
+ formData[part.key] = part.value
394
+ return Effect.succeed(formData)
395
+ }
396
+ const file = part
397
+ const path = path_.join(dir, path_.basename(file.name).slice(-128))
398
+ if (!Array.isArray(formData[part.key])) {
399
+ formData[part.key] = []
400
+ }
401
+ ;(formData[part.key] as Array<FormData.PersistedFile>).push(
402
+ new PersistedFileImpl(
403
+ file.key,
404
+ file.name,
405
+ file.contentType,
406
+ path
407
+ )
408
+ )
409
+ return Effect.as(writeFile(path, file), formData)
410
+ }
411
+ )
412
+ ),
413
+ Effect.catchTags({
414
+ SystemError: (err) => Effect.fail(FormDataError("InternalError", err)),
415
+ BadArgument: (err) => Effect.fail(FormDataError("InternalError", err))
416
+ })
417
+ )
418
+
419
+ class PersistedFileImpl implements FormData.PersistedFile {
420
+ readonly [TypeId]: FormData.TypeId
421
+ readonly _tag = "PersistedFile"
422
+
423
+ constructor(
424
+ readonly key: string,
425
+ readonly name: string,
426
+ readonly contentType: string,
427
+ readonly path: string
428
+ ) {
429
+ this[TypeId] = TypeId
430
+ }
431
+ }
@@ -13,10 +13,7 @@ export const TypeId: ServerRequest.TypeId = Symbol.for("@effect/platform/Http/Se
13
13
  export const serverRequestTag = Context.Tag<ServerRequest.ServerRequest>(TypeId)
14
14
 
15
15
  /** @internal */
16
- export const formDataRecord = Effect.map(
17
- Effect.flatMap(serverRequestTag, (request) => request.formData),
18
- FormData.toRecord
19
- )
16
+ export const persistedFormData = Effect.flatMap(serverRequestTag, (request) => request.formData)
20
17
 
21
18
  /** @internal */
22
19
  export const schemaHeaders = <I extends Readonly<Record<string, string>>, A>(schema: Schema.Schema<I, A>) => {
@@ -37,14 +34,11 @@ export const schemaBodyUrlParams = <I extends Readonly<Record<string, string>>,
37
34
  }
38
35
 
39
36
  /** @internal */
40
- export const schemaFormData = <I extends Readonly<Record<string, string | ReadonlyArray<globalThis.File>>>, A>(
37
+ export const schemaFormData = <I extends FormData.PersistedFormData, A>(
41
38
  schema: Schema.Schema<I, A>
42
39
  ) => {
43
- const parse = FormData.schemaRecord(schema)
44
- return Effect.flatMap(
45
- Effect.flatMap(serverRequestTag, (request) => request.formData),
46
- parse
47
- )
40
+ const parse = FormData.schemaPersisted(schema)
41
+ return Effect.flatMap(persistedFormData, parse)
48
42
  }
49
43
 
50
44
  /** @internal */
@@ -1,5 +1,6 @@
1
- import { Cause, Chunk } from "effect"
1
+ import * as Cause from "effect/Cause"
2
2
  import * as Channel from "effect/Channel"
3
+ import * as Chunk from "effect/Chunk"
3
4
  import * as Context from "effect/Context"
4
5
  import * as Deferred from "effect/Deferred"
5
6
  import * as Effect from "effect/Effect"