@effect/platform 0.85.2 → 0.86.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 (42) hide show
  1. package/dist/cjs/HttpApiBuilder.js +15 -5
  2. package/dist/cjs/HttpApiBuilder.js.map +1 -1
  3. package/dist/cjs/HttpApiSchema.js +6 -6
  4. package/dist/cjs/HttpApiSchema.js.map +1 -1
  5. package/dist/cjs/HttpIncomingMessage.js +7 -5
  6. package/dist/cjs/HttpIncomingMessage.js.map +1 -1
  7. package/dist/cjs/HttpServerRequest.js +10 -4
  8. package/dist/cjs/HttpServerRequest.js.map +1 -1
  9. package/dist/cjs/Multipart.js +400 -47
  10. package/dist/cjs/Multipart.js.map +1 -1
  11. package/dist/dts/HttpApiBuilder.d.ts.map +1 -1
  12. package/dist/dts/HttpApiSchema.d.ts +19 -4
  13. package/dist/dts/HttpApiSchema.d.ts.map +1 -1
  14. package/dist/dts/HttpIncomingMessage.d.ts +5 -2
  15. package/dist/dts/HttpIncomingMessage.d.ts.map +1 -1
  16. package/dist/dts/HttpServerRequest.d.ts +6 -1
  17. package/dist/dts/HttpServerRequest.d.ts.map +1 -1
  18. package/dist/dts/Multipart.d.ts +167 -88
  19. package/dist/dts/Multipart.d.ts.map +1 -1
  20. package/dist/esm/HttpApiBuilder.js +15 -5
  21. package/dist/esm/HttpApiBuilder.js.map +1 -1
  22. package/dist/esm/HttpApiSchema.js +6 -6
  23. package/dist/esm/HttpApiSchema.js.map +1 -1
  24. package/dist/esm/HttpIncomingMessage.js +5 -4
  25. package/dist/esm/HttpIncomingMessage.js.map +1 -1
  26. package/dist/esm/HttpServerRequest.js +6 -1
  27. package/dist/esm/HttpServerRequest.js.map +1 -1
  28. package/dist/esm/Multipart.js +385 -46
  29. package/dist/esm/Multipart.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/HttpApiBuilder.ts +16 -5
  32. package/src/HttpApiSchema.ts +23 -7
  33. package/src/HttpIncomingMessage.ts +5 -7
  34. package/src/HttpServerRequest.ts +6 -1
  35. package/src/Multipart.ts +632 -128
  36. package/dist/cjs/internal/multipart.js +0 -364
  37. package/dist/cjs/internal/multipart.js.map +0 -1
  38. package/dist/dts/internal/multipart.d.ts +0 -2
  39. package/dist/dts/internal/multipart.d.ts.map +0 -1
  40. package/dist/esm/internal/multipart.js +0 -347
  41. package/dist/esm/internal/multipart.js.map +0 -1
  42. package/src/internal/multipart.ts +0 -491
@@ -1,491 +0,0 @@
1
- import type { Cause } from "effect/Cause"
2
- import * as Channel from "effect/Channel"
3
- import * as Chunk from "effect/Chunk"
4
- import * as Effect from "effect/Effect"
5
- import * as Exit from "effect/Exit"
6
- import * as FiberRef from "effect/FiberRef"
7
- import { dual } from "effect/Function"
8
- import { globalValue } from "effect/GlobalValue"
9
- import * as Inspectable from "effect/Inspectable"
10
- import * as Mailbox from "effect/Mailbox"
11
- import * as Option from "effect/Option"
12
- import type * as ParseResult from "effect/ParseResult"
13
- import * as Predicate from "effect/Predicate"
14
- import * as Schema from "effect/Schema"
15
- import type { ParseOptions } from "effect/SchemaAST"
16
- import type * as Scope from "effect/Scope"
17
- import type * as AsyncInput from "effect/SingleProducerAsyncInput"
18
- import * as Stream from "effect/Stream"
19
- import * as MP from "multipasta"
20
- import { TypeIdError } from "../Error.js"
21
- import * as FileSystem from "../FileSystem.js"
22
- import * as IncomingMessage from "../HttpIncomingMessage.js"
23
- import type * as Multipart from "../Multipart.js"
24
- import * as Path from "../Path.js"
25
-
26
- /** @internal */
27
- export const TypeId: Multipart.TypeId = Symbol.for("@effect/platform/Multipart") as Multipart.TypeId
28
-
29
- /** @internal */
30
- export const isPart = (u: unknown): u is Multipart.Part => Predicate.hasProperty(u, TypeId)
31
-
32
- /** @internal */
33
- export const isField = (u: unknown): u is Multipart.Field => isPart(u) && u._tag === "Field"
34
-
35
- /** @internal */
36
- export const isFile = (u: unknown): u is Multipart.File => isPart(u) && u._tag === "File"
37
-
38
- /** @internal */
39
- export const isPersistedFile = (u: unknown): u is Multipart.PersistedFile =>
40
- Predicate.hasProperty(u, TypeId) && Predicate.isTagged(u, "PersistedFile")
41
-
42
- /** @internal */
43
- export const ErrorTypeId: Multipart.ErrorTypeId = Symbol.for(
44
- "@effect/platform/Multipart/MultipartError"
45
- ) as Multipart.ErrorTypeId
46
-
47
- /** @internal */
48
- export class MultipartError extends TypeIdError(ErrorTypeId, "MultipartError")<{
49
- readonly reason: "FileTooLarge" | "FieldTooLarge" | "BodyTooLarge" | "TooManyParts" | "InternalError" | "Parse"
50
- readonly cause: unknown
51
- }> {
52
- get message(): string {
53
- return this.reason
54
- }
55
- }
56
-
57
- /** @internal */
58
- export const maxParts: FiberRef.FiberRef<Option.Option<number>> = globalValue(
59
- "@effect/platform/Multipart/maxParts",
60
- () => FiberRef.unsafeMake(Option.none<number>())
61
- )
62
-
63
- /** @internal */
64
- export const withMaxParts = dual<
65
- (count: Option.Option<number>) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>,
66
- <A, E, R>(effect: Effect.Effect<A, E, R>, count: Option.Option<number>) => Effect.Effect<A, E, R>
67
- >(2, (effect, count) => Effect.locally(effect, maxParts, count))
68
-
69
- /** @internal */
70
- export const maxFieldSize: FiberRef.FiberRef<FileSystem.Size> = globalValue(
71
- "@effect/platform/Multipart/maxFieldSize",
72
- () => FiberRef.unsafeMake(FileSystem.Size(10 * 1024 * 1024))
73
- )
74
-
75
- /** @internal */
76
- export const withMaxFieldSize = dual<
77
- (size: FileSystem.SizeInput) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>,
78
- <A, E, R>(effect: Effect.Effect<A, E, R>, size: FileSystem.SizeInput) => Effect.Effect<A, E, R>
79
- >(2, (effect, size) => Effect.locally(effect, maxFieldSize, FileSystem.Size(size)))
80
-
81
- /** @internal */
82
- export const maxFileSize: FiberRef.FiberRef<Option.Option<FileSystem.Size>> = globalValue(
83
- "@effect/platform/Multipart/maxFileSize",
84
- () => FiberRef.unsafeMake(Option.none<FileSystem.Size>())
85
- )
86
-
87
- /** @internal */
88
- export const withMaxFileSize = dual<
89
- (size: Option.Option<FileSystem.SizeInput>) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>,
90
- <A, E, R>(effect: Effect.Effect<A, E, R>, size: Option.Option<FileSystem.SizeInput>) => Effect.Effect<A, E, R>
91
- >(2, (effect, size) => Effect.locally(effect, maxFileSize, Option.map(size, FileSystem.Size)))
92
-
93
- /** @internal */
94
- export const fieldMimeTypes: FiberRef.FiberRef<Chunk.Chunk<string>> = globalValue(
95
- "@effect/platform/Multipart/fieldMimeTypes",
96
- () => FiberRef.unsafeMake<Chunk.Chunk<string>>(Chunk.make("application/json"))
97
- )
98
-
99
- /** @internal */
100
- export const withFieldMimeTypes = dual<
101
- (mimeTypes: ReadonlyArray<string>) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>,
102
- <A, E, R>(effect: Effect.Effect<A, E, R>, mimeTypes: ReadonlyArray<string>) => Effect.Effect<A, E, R>
103
- >(2, (effect, mimeTypes) => Effect.locally(effect, fieldMimeTypes, Chunk.fromIterable(mimeTypes)))
104
-
105
- /** @internal */
106
- export const FileSchema: Schema.Schema<Multipart.PersistedFile> = Schema.declare(isPersistedFile, {
107
- identifier: "PersistedFile",
108
- jsonSchema: {
109
- type: "string",
110
- format: "binary"
111
- }
112
- })
113
-
114
- /** @internal */
115
- export const FilesSchema: Schema.Schema<ReadonlyArray<Multipart.PersistedFile>> = Schema.Array(FileSchema)
116
-
117
- /** @internal */
118
- export const SingleFileSchema: Schema.transform<
119
- Schema.Schema<ReadonlyArray<Multipart.PersistedFile>>,
120
- Schema.Schema<Multipart.PersistedFile>
121
- > = Schema.transform(FilesSchema.pipe(Schema.itemsCount(1)), FileSchema, {
122
- strict: true,
123
- decode: ([file]) => file,
124
- encode: (file) => [file]
125
- })
126
-
127
- /** @internal */
128
- export const schemaPersisted = <A, I extends Partial<Multipart.Persisted>, R>(
129
- schema: Schema.Schema<A, I, R>,
130
- options?: ParseOptions | undefined
131
- ) => {
132
- const parse = Schema.decodeUnknown(schema, options)
133
- return (persisted: Multipart.Persisted) => parse(persisted)
134
- }
135
-
136
- /** @internal */
137
- export const schemaJson = <A, I, R>(schema: Schema.Schema<A, I, R>, options?: ParseOptions | undefined): {
138
- (
139
- field: string
140
- ): (persisted: Multipart.Persisted) => Effect.Effect<A, ParseResult.ParseError, R>
141
- (
142
- persisted: Multipart.Persisted,
143
- field: string
144
- ): Effect.Effect<A, ParseResult.ParseError, R>
145
- } => {
146
- const fromJson = Schema.parseJson(schema)
147
- return dual<
148
- (
149
- field: string
150
- ) => (
151
- persisted: Multipart.Persisted
152
- ) => Effect.Effect<A, ParseResult.ParseError, R>,
153
- (
154
- persisted: Multipart.Persisted,
155
- field: string
156
- ) => Effect.Effect<A, ParseResult.ParseError, R>
157
- >(2, (persisted, field) =>
158
- Effect.map(
159
- Schema.decodeUnknown(
160
- Schema.Struct({
161
- [field]: fromJson
162
- }),
163
- options
164
- )(persisted),
165
- (_) => _[field]
166
- ))
167
- }
168
-
169
- /** @internal */
170
- export const makeConfig = (
171
- headers: Record<string, string>
172
- ): Effect.Effect<MP.BaseConfig> =>
173
- Effect.withFiberRuntime((fiber) => {
174
- const mimeTypes = fiber.getFiberRef(fieldMimeTypes)
175
- return Effect.succeed<MP.BaseConfig>({
176
- headers,
177
- maxParts: Option.getOrUndefined(fiber.getFiberRef(maxParts)),
178
- maxFieldSize: Number(fiber.getFiberRef(maxFieldSize)),
179
- maxPartSize: fiber.getFiberRef(maxFileSize).pipe(Option.map(Number), Option.getOrUndefined),
180
- maxTotalSize: fiber.getFiberRef(IncomingMessage.maxBodySize).pipe(Option.map(Number), Option.getOrUndefined),
181
- isFile: mimeTypes.length === 0 ? undefined : (info: MP.PartInfo): boolean =>
182
- !Chunk.some(
183
- mimeTypes,
184
- (_) => info.contentType.includes(_)
185
- ) && MP.defaultIsFile(info)
186
- })
187
- })
188
-
189
- /** @internal */
190
- export const makeChannel = <IE>(
191
- headers: Record<string, string>,
192
- bufferSize = 16
193
- ): Channel.Channel<
194
- Chunk.Chunk<Multipart.Part>,
195
- Chunk.Chunk<Uint8Array>,
196
- Multipart.MultipartError | IE,
197
- IE,
198
- unknown,
199
- unknown
200
- > =>
201
- Channel.acquireUseRelease(
202
- Effect.all([
203
- makeConfig(headers),
204
- Mailbox.make<Chunk.Chunk<Uint8Array>>(bufferSize)
205
- ]),
206
- ([config, mailbox]) => {
207
- let partsBuffer: Array<Multipart.Part> = []
208
- let exit = Option.none<Exit.Exit<void, IE | Multipart.MultipartError>>()
209
-
210
- const input: AsyncInput.AsyncInputProducer<IE, Chunk.Chunk<Uint8Array>, unknown> = {
211
- awaitRead: () => Effect.void,
212
- emit(element) {
213
- return mailbox.offer(element)
214
- },
215
- error(cause) {
216
- exit = Option.some(Exit.failCause(cause))
217
- return mailbox.end
218
- },
219
- done(_value) {
220
- return mailbox.end
221
- }
222
- }
223
-
224
- const parser = MP.make({
225
- ...config,
226
- onField(info, value) {
227
- partsBuffer.push(new FieldImpl(info.name, info.contentType, MP.decodeField(info, value)))
228
- },
229
- onFile(info) {
230
- let chunks: Array<Uint8Array> = []
231
- let finished = false
232
- const take: Channel.Channel<Chunk.Chunk<Uint8Array>> = Channel.suspend(() => {
233
- if (chunks.length === 0) {
234
- return finished ? Channel.void : Channel.zipRight(pump, take)
235
- }
236
- const chunk = Chunk.unsafeFromArray(chunks)
237
- chunks = []
238
- return finished ? Channel.write(chunk) : Channel.zipRight(
239
- Channel.write(chunk),
240
- Channel.zipRight(pump, take)
241
- )
242
- })
243
- partsBuffer.push(new FileImpl(info, take))
244
- return function(chunk) {
245
- if (chunk === null) {
246
- finished = true
247
- } else {
248
- chunks.push(chunk)
249
- }
250
- }
251
- },
252
- onError(error_) {
253
- exit = Option.some(Exit.fail(convertError(error_)))
254
- },
255
- onDone() {
256
- exit = Option.some(Exit.void)
257
- }
258
- })
259
-
260
- const pump = Channel.flatMap(
261
- mailbox.takeAll,
262
- ([chunks, done]) =>
263
- Channel.sync(() => {
264
- Chunk.forEach(chunks, Chunk.forEach(parser.write))
265
- if (done) {
266
- parser.end()
267
- }
268
- })
269
- )
270
-
271
- const partsChannel: Channel.Channel<
272
- Chunk.Chunk<Multipart.Part>,
273
- unknown,
274
- IE | Multipart.MultipartError
275
- > = Channel.flatMap(
276
- pump,
277
- () => {
278
- if (partsBuffer.length === 0) {
279
- return exit._tag === "None" ? partsChannel : writeExit(exit.value)
280
- }
281
- const chunk = Chunk.unsafeFromArray(partsBuffer)
282
- partsBuffer = []
283
- return Channel.zipRight(
284
- Channel.write(chunk),
285
- exit._tag === "None" ? partsChannel : writeExit(exit.value)
286
- )
287
- }
288
- )
289
-
290
- return Channel.embedInput(partsChannel, input)
291
- },
292
- ([, mailbox]) => mailbox.shutdown
293
- )
294
-
295
- const writeExit = <A, E>(
296
- self: Exit.Exit<A, E>
297
- ): Channel.Channel<never, unknown, E> => self._tag === "Success" ? Channel.void : Channel.failCause(self.cause)
298
-
299
- function convertError(cause: MP.MultipartError): Multipart.MultipartError {
300
- switch (cause._tag) {
301
- case "ReachedLimit": {
302
- switch (cause.limit) {
303
- case "MaxParts": {
304
- return new MultipartError({ reason: "TooManyParts", cause })
305
- }
306
- case "MaxFieldSize": {
307
- return new MultipartError({ reason: "FieldTooLarge", cause })
308
- }
309
- case "MaxPartSize": {
310
- return new MultipartError({ reason: "FileTooLarge", cause })
311
- }
312
- case "MaxTotalSize": {
313
- return new MultipartError({ reason: "BodyTooLarge", cause })
314
- }
315
- }
316
- }
317
- default: {
318
- return new MultipartError({ reason: "Parse", cause })
319
- }
320
- }
321
- }
322
-
323
- abstract class PartBase extends Inspectable.Class {
324
- readonly [TypeId]: Multipart.TypeId
325
- constructor() {
326
- super()
327
- this[TypeId] = TypeId
328
- }
329
- }
330
-
331
- class FieldImpl extends PartBase implements Multipart.Field {
332
- readonly _tag = "Field"
333
-
334
- constructor(
335
- readonly key: string,
336
- readonly contentType: string,
337
- readonly value: string
338
- ) {
339
- super()
340
- }
341
-
342
- toJSON(): unknown {
343
- return {
344
- _id: "@effect/platform/Multipart/Part",
345
- _tag: "Field",
346
- key: this.key,
347
- contentType: this.contentType,
348
- value: this.value
349
- }
350
- }
351
- }
352
-
353
- class FileImpl extends PartBase implements Multipart.File {
354
- readonly _tag = "File"
355
- readonly key: string
356
- readonly name: string
357
- readonly contentType: string
358
- readonly content: Stream.Stream<Uint8Array, Multipart.MultipartError>
359
- readonly contentEffect: Effect.Effect<Uint8Array, Multipart.MultipartError>
360
-
361
- constructor(
362
- info: MP.PartInfo,
363
- channel: Channel.Channel<Chunk.Chunk<Uint8Array>, unknown, never, unknown, void, unknown>
364
- ) {
365
- super()
366
- this.key = info.name
367
- this.name = info.filename ?? info.name
368
- this.contentType = info.contentType
369
- this.content = Stream.fromChannel(channel)
370
- this.contentEffect = channel.pipe(
371
- Channel.pipeTo(collectUint8Array),
372
- Channel.run,
373
- Effect.mapError((cause) => new MultipartError({ reason: "InternalError", cause }))
374
- )
375
- }
376
-
377
- toJSON(): unknown {
378
- return {
379
- _id: "@effect/platform/Multipart/Part",
380
- _tag: "File",
381
- key: this.key,
382
- name: this.name,
383
- contentType: this.contentType
384
- }
385
- }
386
- }
387
-
388
- const defaultWriteFile = (path: string, file: Multipart.File) =>
389
- Effect.flatMap(
390
- FileSystem.FileSystem,
391
- (fs) =>
392
- Effect.mapError(
393
- Stream.run(file.content, fs.sink(path)),
394
- (cause) => new MultipartError({ reason: "InternalError", cause })
395
- )
396
- )
397
-
398
- /** @internal */
399
- export const collectUint8Array = Channel.suspend(() => {
400
- let accumulator = new Uint8Array(0)
401
- const loop: Channel.Channel<
402
- never,
403
- Chunk.Chunk<Uint8Array>,
404
- unknown,
405
- unknown,
406
- Uint8Array
407
- > = Channel.readWithCause({
408
- onInput(chunk: Chunk.Chunk<Uint8Array>) {
409
- for (const element of chunk) {
410
- const newAccumulator = new Uint8Array(accumulator.length + element.length)
411
- newAccumulator.set(accumulator, 0)
412
- newAccumulator.set(element, accumulator.length)
413
- accumulator = newAccumulator
414
- }
415
- return loop
416
- },
417
- onFailure: (cause: Cause<unknown>) => Channel.failCause(cause),
418
- onDone: () => Channel.succeed(accumulator)
419
- })
420
- return loop
421
- })
422
-
423
- /** @internal */
424
- export const toPersisted = (
425
- stream: Stream.Stream<Multipart.Part, Multipart.MultipartError>,
426
- writeFile = defaultWriteFile
427
- ): Effect.Effect<Multipart.Persisted, Multipart.MultipartError, FileSystem.FileSystem | Path.Path | Scope.Scope> =>
428
- Effect.gen(function*() {
429
- const fs = yield* FileSystem.FileSystem
430
- const path_ = yield* Path.Path
431
- const dir = yield* fs.makeTempDirectoryScoped()
432
- const persisted: Record<string, Array<Multipart.PersistedFile> | Array<string> | string> = Object.create(null)
433
- yield* Stream.runForEach(stream, (part) => {
434
- if (part._tag === "Field") {
435
- if (!(part.key in persisted)) {
436
- persisted[part.key] = part.value
437
- } else if (typeof persisted[part.key] === "string") {
438
- persisted[part.key] = [persisted[part.key] as string, part.value]
439
- } else {
440
- ;(persisted[part.key] as Array<string>).push(part.value)
441
- }
442
- return Effect.void
443
- } else if (part.name === "") {
444
- return Effect.void
445
- }
446
- const file = part
447
- const path = path_.join(dir, path_.basename(file.name).slice(-128))
448
- const filePart = new PersistedFileImpl(
449
- file.key,
450
- file.name,
451
- file.contentType,
452
- path
453
- )
454
- if (Array.isArray(persisted[part.key])) {
455
- ;(persisted[part.key] as Array<Multipart.PersistedFile>).push(filePart)
456
- } else {
457
- persisted[part.key] = [filePart]
458
- }
459
- return writeFile(path, file)
460
- })
461
- return persisted
462
- }).pipe(
463
- Effect.catchTags({
464
- SystemError: (cause) => Effect.fail(new MultipartError({ reason: "InternalError", cause })),
465
- BadArgument: (cause) => Effect.fail(new MultipartError({ reason: "InternalError", cause }))
466
- })
467
- )
468
-
469
- class PersistedFileImpl extends PartBase implements Multipart.PersistedFile {
470
- readonly _tag = "PersistedFile"
471
-
472
- constructor(
473
- readonly key: string,
474
- readonly name: string,
475
- readonly contentType: string,
476
- readonly path: string
477
- ) {
478
- super()
479
- }
480
-
481
- toJSON(): unknown {
482
- return {
483
- _id: "@effect/platform/Multipart/Part",
484
- _tag: "PersistedFile",
485
- key: this.key,
486
- name: this.name,
487
- contentType: this.contentType,
488
- path: this.path
489
- }
490
- }
491
- }