@effect/platform 0.63.2 → 0.64.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 (54) hide show
  1. package/dist/cjs/Cookies.js +0 -1
  2. package/dist/cjs/Cookies.js.map +1 -1
  3. package/dist/cjs/Etag.js +11 -1
  4. package/dist/cjs/Etag.js.map +1 -1
  5. package/dist/cjs/HttpApiBuilder.js +35 -1
  6. package/dist/cjs/HttpApiBuilder.js.map +1 -1
  7. package/dist/cjs/HttpPlatform.js +6 -1
  8. package/dist/cjs/HttpPlatform.js.map +1 -1
  9. package/dist/cjs/Socket.js +65 -34
  10. package/dist/cjs/Socket.js.map +1 -1
  11. package/dist/cjs/internal/etag.js +42 -1
  12. package/dist/cjs/internal/etag.js.map +1 -1
  13. package/dist/cjs/internal/httpPlatform.js +28 -4
  14. package/dist/cjs/internal/httpPlatform.js.map +1 -1
  15. package/dist/cjs/internal/httpServerResponse.js +5 -4
  16. package/dist/cjs/internal/httpServerResponse.js.map +1 -1
  17. package/dist/cjs/internal/multipart.js +1 -3
  18. package/dist/cjs/internal/multipart.js.map +1 -1
  19. package/dist/dts/Etag.d.ts +11 -0
  20. package/dist/dts/Etag.d.ts.map +1 -1
  21. package/dist/dts/HttpApiBuilder.d.ts +29 -1
  22. package/dist/dts/HttpApiBuilder.d.ts.map +1 -1
  23. package/dist/dts/HttpPlatform.d.ts +6 -0
  24. package/dist/dts/HttpPlatform.d.ts.map +1 -1
  25. package/dist/dts/Socket.d.ts +20 -3
  26. package/dist/dts/Socket.d.ts.map +1 -1
  27. package/dist/esm/Cookies.js +0 -1
  28. package/dist/esm/Cookies.js.map +1 -1
  29. package/dist/esm/Etag.js +10 -0
  30. package/dist/esm/Etag.js.map +1 -1
  31. package/dist/esm/HttpApiBuilder.js +33 -0
  32. package/dist/esm/HttpApiBuilder.js.map +1 -1
  33. package/dist/esm/HttpPlatform.js +5 -0
  34. package/dist/esm/HttpPlatform.js.map +1 -1
  35. package/dist/esm/Socket.js +62 -33
  36. package/dist/esm/Socket.js.map +1 -1
  37. package/dist/esm/internal/etag.js +41 -0
  38. package/dist/esm/internal/etag.js.map +1 -1
  39. package/dist/esm/internal/httpPlatform.js +28 -4
  40. package/dist/esm/internal/httpPlatform.js.map +1 -1
  41. package/dist/esm/internal/httpServerResponse.js +4 -3
  42. package/dist/esm/internal/httpServerResponse.js.map +1 -1
  43. package/dist/esm/internal/multipart.js +1 -3
  44. package/dist/esm/internal/multipart.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/Cookies.ts +1 -1
  47. package/src/Etag.ts +13 -0
  48. package/src/HttpApiBuilder.ts +46 -2
  49. package/src/HttpPlatform.ts +7 -0
  50. package/src/Socket.ts +113 -52
  51. package/src/internal/etag.ts +43 -0
  52. package/src/internal/httpPlatform.ts +32 -4
  53. package/src/internal/httpServerResponse.ts +6 -3
  54. package/src/internal/multipart.ts +1 -3
package/src/Socket.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import * as Cause from "effect/Cause"
5
4
  import * as Channel from "effect/Channel"
6
5
  import * as Chunk from "effect/Chunk"
7
6
  import * as Context from "effect/Context"
@@ -12,6 +11,7 @@ import * as ExecutionStrategy from "effect/ExecutionStrategy"
12
11
  import * as Exit from "effect/Exit"
13
12
  import * as FiberRef from "effect/FiberRef"
14
13
  import * as FiberSet from "effect/FiberSet"
14
+ import { dual } from "effect/Function"
15
15
  import { globalValue } from "effect/GlobalValue"
16
16
  import * as Layer from "effect/Layer"
17
17
  import * as Predicate from "effect/Predicate"
@@ -32,6 +32,12 @@ export const TypeId: unique symbol = Symbol.for("@effect/platform/Socket")
32
32
  */
33
33
  export type TypeId = typeof TypeId
34
34
 
35
+ /**
36
+ * @since 1.0.0
37
+ * @category guards
38
+ */
39
+ export const isSocket = (u: unknown): u is Socket => Predicate.hasProperty(u, TypeId)
40
+
35
41
  /**
36
42
  * @since 1.0.0
37
43
  * @category tags
@@ -171,10 +177,11 @@ export class SocketCloseError extends TypeIdError(SocketErrorTypeId, "SocketErro
171
177
  * @since 1.0.0
172
178
  * @category combinators
173
179
  */
174
- export const toChannel = <IE>(
175
- self: Socket
180
+ export const toChannelMap = <IE, A>(
181
+ self: Socket,
182
+ f: (data: Uint8Array | string) => A
176
183
  ): Channel.Channel<
177
- Chunk.Chunk<Uint8Array>,
184
+ Chunk.Chunk<A>,
178
185
  Chunk.Chunk<Uint8Array | string | CloseEvent>,
179
186
  SocketError | IE,
180
187
  IE,
@@ -183,25 +190,27 @@ export const toChannel = <IE>(
183
190
  > =>
184
191
  Effect.scope.pipe(
185
192
  Effect.bindTo("scope"),
193
+ Effect.let("state", () => ({ finished: false, buffer: [] as Array<A> })),
194
+ Effect.bind("semaphore", () => Effect.makeSemaphore(0)),
186
195
  Effect.bind("writeScope", ({ scope }) => Scope.fork(scope, ExecutionStrategy.sequential)),
187
196
  Effect.bind("write", ({ writeScope }) => Scope.extend(self.writer, writeScope)),
188
- Effect.bind("exitQueue", (_) => Queue.unbounded<Exit.Exit<Chunk.Chunk<Uint8Array>, SocketError | IE>>()),
197
+ Effect.bind("deferred", () => Deferred.make<void, SocketError | IE>()),
189
198
  Effect.let(
190
199
  "input",
191
200
  (
192
- { exitQueue, write, writeScope }
201
+ { deferred, write, writeScope }
193
202
  ): AsyncProducer.AsyncInputProducer<IE, Chunk.Chunk<Uint8Array | string | CloseEvent>, unknown> => ({
194
203
  awaitRead: () => Effect.void,
195
204
  emit(chunk) {
196
205
  return Effect.catchAllCause(
197
206
  Effect.forEach(chunk, write, { discard: true }),
198
- (cause) => Queue.offer(exitQueue, Exit.failCause(cause))
207
+ (cause) => Deferred.failCause(deferred, cause)
199
208
  )
200
209
  },
201
210
  error(error) {
202
211
  return Effect.zipRight(
203
212
  Scope.close(writeScope, Exit.void),
204
- Queue.offer(exitQueue, Exit.failCause(error))
213
+ Deferred.failCause(deferred, error)
205
214
  )
206
215
  },
207
216
  done() {
@@ -209,30 +218,95 @@ export const toChannel = <IE>(
209
218
  }
210
219
  })
211
220
  ),
212
- Effect.tap(({ exitQueue }) =>
213
- self.run((data) => Queue.offer(exitQueue, Exit.succeed(Chunk.of(data)))).pipe(
214
- Effect.zipRight(Effect.failCause(Cause.empty)),
215
- Effect.exit,
216
- Effect.tap((exit) => Queue.offer(exitQueue, exit)),
217
- Effect.fork,
221
+ Effect.tap(({ deferred, scope, semaphore, state }) =>
222
+ self.runRaw((data) => {
223
+ state.buffer.push(f(data))
224
+ return semaphore.release(1)
225
+ }).pipe(
226
+ Effect.intoDeferred(deferred),
227
+ Effect.raceFirst(Deferred.await(deferred)),
228
+ Effect.ensuring(Effect.suspend(() => {
229
+ state.finished = true
230
+ return semaphore.release(1)
231
+ })),
232
+ Effect.forkIn(scope),
218
233
  Effect.interruptible
219
234
  )
220
235
  ),
221
- Effect.map(({ exitQueue, input }) => {
222
- const loop: Channel.Channel<Chunk.Chunk<Uint8Array>, unknown, SocketError | IE, unknown, void, unknown> = Channel
223
- .flatMap(
224
- Queue.take(exitQueue),
225
- Exit.match({
226
- onFailure: (cause) => Cause.isEmptyType(cause) ? Channel.void : Channel.failCause(cause),
227
- onSuccess: (chunk) => Channel.zipRight(Channel.write(chunk), loop)
228
- })
229
- )
230
-
236
+ Effect.map(({ deferred, input, semaphore, state }) => {
237
+ const loop: Channel.Channel<Chunk.Chunk<A>, unknown, SocketError | IE, unknown, void, unknown> = Channel.flatMap(
238
+ semaphore.take(1),
239
+ (_) => {
240
+ if (state.buffer.length === 0) {
241
+ return state.finished ? Deferred.await(deferred) : loop
242
+ }
243
+ const chunk = Chunk.unsafeFromArray(state.buffer)
244
+ state.buffer = []
245
+ return Channel.zipRight(Channel.write(chunk), state.finished ? Deferred.await(deferred) : loop)
246
+ }
247
+ )
231
248
  return Channel.embedInput(loop, input)
232
249
  }),
233
250
  Channel.unwrapScoped
234
251
  )
235
252
 
253
+ /**
254
+ * @since 1.0.0
255
+ * @category combinators
256
+ */
257
+ export const toChannel = <IE>(
258
+ self: Socket
259
+ ): Channel.Channel<
260
+ Chunk.Chunk<Uint8Array>,
261
+ Chunk.Chunk<Uint8Array | string | CloseEvent>,
262
+ SocketError | IE,
263
+ IE,
264
+ void,
265
+ unknown
266
+ > => {
267
+ const encoder = new TextEncoder()
268
+ return toChannelMap(self, (data) => typeof data === "string" ? encoder.encode(data) : data)
269
+ }
270
+
271
+ /**
272
+ * @since 1.0.0
273
+ * @category combinators
274
+ */
275
+ export const toChannelString: {
276
+ (encoding?: string | undefined): <IE>(self: Socket) => Channel.Channel<
277
+ Chunk.Chunk<string>,
278
+ Chunk.Chunk<Uint8Array | string | CloseEvent>,
279
+ SocketError | IE,
280
+ IE,
281
+ void,
282
+ unknown
283
+ >
284
+ <IE>(
285
+ self: Socket,
286
+ encoding?: string | undefined
287
+ ): Channel.Channel<
288
+ Chunk.Chunk<string>,
289
+ Chunk.Chunk<Uint8Array | string | CloseEvent>,
290
+ SocketError | IE,
291
+ IE,
292
+ void,
293
+ unknown
294
+ >
295
+ } = dual((args) => isSocket(args[0]), <IE>(
296
+ self: Socket,
297
+ encoding?: string | undefined
298
+ ): Channel.Channel<
299
+ Chunk.Chunk<string>,
300
+ Chunk.Chunk<Uint8Array | string | CloseEvent>,
301
+ SocketError | IE,
302
+ IE,
303
+ void,
304
+ unknown
305
+ > => {
306
+ const decoder = new TextDecoder(encoding)
307
+ return toChannelMap(self, (data) => typeof data === "string" ? data : decoder.decode(data))
308
+ })
309
+
236
310
  /**
237
311
  * @since 1.0.0
238
312
  * @category combinators
@@ -348,16 +422,12 @@ export const fromWebSocket = <R>(
348
422
  Effect.map(
349
423
  Queue.dropping<Uint8Array | string | CloseEvent>(fiber.getFiberRef(currentSendQueueCapacity)),
350
424
  (sendQueue) => {
351
- const acquireContext = fiber.getFiberRef(FiberRef.currentContext) as Context.Context<Exclude<R, Scope.Scope>>
425
+ const acquireContext = fiber.getFiberRef(FiberRef.currentContext) as Context.Context<R>
352
426
  const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError
353
427
  const runRaw = <_, E, R>(handler: (_: string | Uint8Array) => Effect.Effect<_, E, R>) =>
354
- Effect.scope.pipe(
355
- Effect.bindTo("scope"),
356
- Effect.bind("ws", ({ scope }) =>
357
- acquire.pipe(
358
- Effect.provide(Context.add(acquireContext, Scope.Scope, scope))
359
- ) as Effect.Effect<globalThis.WebSocket>),
360
- Effect.bind("fiberSet", (_) => FiberSet.make<any, E | SocketError>()),
428
+ acquire.pipe(
429
+ Effect.bindTo("ws"),
430
+ Effect.bind("fiberSet", () => FiberSet.make<any, E | SocketError>()),
361
431
  Effect.bind("run", ({ fiberSet, ws }) =>
362
432
  Effect.provideService(FiberSet.runtime(fiberSet)<R>(), WebSocket, ws)),
363
433
  Effect.tap(({ fiberSet, run, ws }) => {
@@ -392,19 +462,12 @@ export const fromWebSocket = <R>(
392
462
  }
393
463
 
394
464
  if (ws.readyState !== 1) {
395
- return Effect.async<void, SocketError, never>((resume) => {
396
- function onOpen() {
397
- ws.removeEventListener("open", onOpen)
398
- resume(Effect.void)
399
- }
400
- ws.addEventListener("open", onOpen)
401
- return Effect.sync(() => {
402
- ws.removeEventListener("open", onOpen)
403
- })
404
- }).pipe(
405
- Effect.tap((_) => {
406
- open = true
407
- }),
465
+ const openDeferred = Deferred.unsafeMake<void>(fiber.id())
466
+ ws.onopen = () => {
467
+ open = true
468
+ Deferred.unsafeDone(openDeferred, Effect.void)
469
+ }
470
+ return Deferred.await(openDeferred).pipe(
408
471
  Effect.timeoutFail({
409
472
  duration: options?.openTimeout ?? 10000,
410
473
  onTimeout: () =>
@@ -445,6 +508,7 @@ export const fromWebSocket = <R>(
445
508
  (_) => Effect.void
446
509
  )
447
510
  ),
511
+ Effect.mapInputContext((input: Context.Context<R | Scope.Scope>) => Context.merge(acquireContext, input)),
448
512
  Effect.scoped,
449
513
  Effect.interruptible
450
514
  )
@@ -534,15 +598,11 @@ export const fromTransformStream = <R>(acquire: Effect.Effect<InputTransformStre
534
598
  Effect.map(
535
599
  Queue.dropping<Uint8Array | string | CloseEvent | typeof EOF>(fiber.getFiberRef(currentSendQueueCapacity)),
536
600
  (sendQueue) => {
537
- const acquireContext = fiber.getFiberRef(FiberRef.currentContext) as Context.Context<Exclude<R, Scope.Scope>>
601
+ const acquireContext = fiber.getFiberRef(FiberRef.currentContext) as Context.Context<R>
538
602
  const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError
539
603
  const runRaw = <_, E, R>(handler: (_: string | Uint8Array) => Effect.Effect<_, E, R>) =>
540
- Effect.scope.pipe(
541
- Effect.bindTo("scope"),
542
- Effect.bind("stream", ({ scope }) =>
543
- acquire.pipe(
544
- Effect.provide(Context.add(acquireContext, Scope.Scope, scope))
545
- ) as Effect.Effect<InputTransformStream>),
604
+ acquire.pipe(
605
+ Effect.bindTo("stream"),
546
606
  Effect.bind("reader", ({ stream }) =>
547
607
  Effect.acquireRelease(
548
608
  Effect.sync(() => stream.readable.getReader()),
@@ -615,6 +675,7 @@ export const fromTransformStream = <R>(acquire: Effect.Effect<InputTransformStre
615
675
  (_) => Effect.void
616
676
  )
617
677
  ),
678
+ Effect.mapInputContext((input: Context.Context<R | Scope.Scope>) => Context.merge(acquireContext, input)),
618
679
  Effect.scoped,
619
680
  Effect.interruptible
620
681
  )
@@ -1,5 +1,9 @@
1
1
  import * as Context from "effect/Context"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Layer from "effect/Layer"
2
4
  import type * as Etag from "../Etag.js"
5
+ import type * as FileSystem from "../FileSystem.js"
6
+ import type * as Body from "../HttpBody.js"
3
7
 
4
8
  /** @internal */
5
9
  export const GeneratorTypeId: Etag.GeneratorTypeId = Symbol.for(
@@ -18,3 +22,42 @@ export const toString = (self: Etag.Etag): string => {
18
22
  return `"${self.value}"`
19
23
  }
20
24
  }
25
+
26
+ const fromFileInfo = (info: FileSystem.File.Info) => {
27
+ const mtime = info.mtime._tag === "Some"
28
+ ? info.mtime.value.getTime().toString(16)
29
+ : "0"
30
+ return `${info.size.toString(16)}-${mtime}`
31
+ }
32
+
33
+ const fromFileWeb = (file: Body.HttpBody.FileLike) => {
34
+ return `${file.size.toString(16)}-${file.lastModified.toString(16)}`
35
+ }
36
+
37
+ /** @internal */
38
+ export const layer = Layer.succeed(
39
+ tag,
40
+ tag.of({
41
+ [GeneratorTypeId]: GeneratorTypeId,
42
+ fromFileInfo(info) {
43
+ return Effect.sync(() => ({ _tag: "Strong", value: fromFileInfo(info) }))
44
+ },
45
+ fromFileWeb(file) {
46
+ return Effect.sync(() => ({ _tag: "Strong", value: fromFileWeb(file) }))
47
+ }
48
+ })
49
+ )
50
+
51
+ /** @internal */
52
+ export const layerWeak = Layer.succeed(
53
+ tag,
54
+ tag.of({
55
+ [GeneratorTypeId]: GeneratorTypeId,
56
+ fromFileInfo(info) {
57
+ return Effect.sync(() => ({ _tag: "Weak", value: fromFileInfo(info) }))
58
+ },
59
+ fromFileWeb(file) {
60
+ return Effect.sync(() => ({ _tag: "Weak", value: fromFileWeb(file) }))
61
+ }
62
+ })
63
+ )
@@ -1,12 +1,15 @@
1
1
  import * as Context from "effect/Context"
2
2
  import * as Effect from "effect/Effect"
3
- import { pipe } from "effect/Function"
3
+ import { identity, pipe } from "effect/Function"
4
+ import * as Layer from "effect/Layer"
5
+ import * as Stream from "effect/Stream"
4
6
  import * as Etag from "../Etag.js"
5
7
  import * as FileSystem from "../FileSystem.js"
6
8
  import * as Headers from "../Headers.js"
7
9
  import type * as Body from "../HttpBody.js"
8
10
  import type * as Platform from "../HttpPlatform.js"
9
11
  import type * as ServerResponse from "../HttpServerResponse.js"
12
+ import * as serverResponse from "./httpServerResponse.js"
10
13
 
11
14
  /** @internal */
12
15
  export const TypeId: Platform.TypeId = Symbol.for("@effect/platform/HttpPlatform") as Platform.TypeId
@@ -33,9 +36,9 @@ export const make = (impl: {
33
36
  options?: FileSystem.StreamOptions
34
37
  ) => ServerResponse.HttpServerResponse
35
38
  }): Effect.Effect<Platform.HttpPlatform, never, FileSystem.FileSystem | Etag.Generator> =>
36
- Effect.gen(function*(_) {
37
- const fs = yield* _(FileSystem.FileSystem)
38
- const etagGen = yield* _(Etag.Generator)
39
+ Effect.gen(function*() {
40
+ const fs = yield* FileSystem.FileSystem
41
+ const etagGen = yield* Etag.Generator
39
42
 
40
43
  return tag.of({
41
44
  [TypeId]: TypeId,
@@ -83,3 +86,28 @@ export const make = (impl: {
83
86
  }
84
87
  })
85
88
  })
89
+
90
+ /** @internal */
91
+ export const layer = Layer.effect(
92
+ tag,
93
+ Effect.flatMap(FileSystem.FileSystem, (fs) =>
94
+ make({
95
+ fileResponse(path, status, statusText, headers, start, end, contentLength) {
96
+ return serverResponse.stream(
97
+ fs.stream(path, {
98
+ offset: start,
99
+ bytesToRead: end !== undefined ? end - start : undefined
100
+ }),
101
+ { contentLength, headers, status, statusText }
102
+ )
103
+ },
104
+ fileWebResponse(file, status, statusText, headers, _options) {
105
+ return serverResponse.stream(
106
+ Stream.fromReadableStream(() => file.stream() as ReadableStream<Uint8Array>, identity),
107
+ { headers, status, statusText }
108
+ )
109
+ }
110
+ }))
111
+ ).pipe(
112
+ Layer.provide(Etag.layerWeak)
113
+ )
@@ -1,5 +1,6 @@
1
1
  import type { ParseOptions } from "@effect/schema/AST"
2
2
  import type * as Schema from "@effect/schema/Schema"
3
+ import * as Context from "effect/Context"
3
4
  import * as Effect from "effect/Effect"
4
5
  import * as Effectable from "effect/Effectable"
5
6
  import { dual } from "effect/Function"
@@ -11,7 +12,7 @@ import type * as PlatformError from "../Error.js"
11
12
  import type * as FileSystem from "../FileSystem.js"
12
13
  import * as Headers from "../Headers.js"
13
14
  import type * as Body from "../HttpBody.js"
14
- import * as Platform from "../HttpPlatform.js"
15
+ import type * as Platform from "../HttpPlatform.js"
15
16
  import type * as Respondable from "../HttpServerRespondable.js"
16
17
  import type * as ServerResponse from "../HttpServerResponse.js"
17
18
  import * as Template from "../Template.js"
@@ -214,13 +215,15 @@ export const schemaJson = <A, I, R>(
214
215
  ))
215
216
  }
216
217
 
218
+ const httpPlatform = Context.GenericTag<Platform.HttpPlatform>("@effect/platform/HttpPlatform")
219
+
217
220
  /** @internal */
218
221
  export const file = (
219
222
  path: string,
220
223
  options?: (ServerResponse.Options & FileSystem.StreamOptions) | undefined
221
224
  ): Effect.Effect<ServerResponse.HttpServerResponse, PlatformError.PlatformError, Platform.HttpPlatform> =>
222
225
  Effect.flatMap(
223
- Platform.HttpPlatform,
226
+ httpPlatform,
224
227
  (platform) => platform.fileResponse(path, options)
225
228
  )
226
229
 
@@ -230,7 +233,7 @@ export const fileWeb = (
230
233
  options?: (ServerResponse.Options.WithContent & FileSystem.StreamOptions) | undefined
231
234
  ): Effect.Effect<ServerResponse.HttpServerResponse, never, Platform.HttpPlatform> =>
232
235
  Effect.flatMap(
233
- Platform.HttpPlatform,
236
+ httpPlatform,
234
237
  (platform) => platform.fileWebResponse(file, options)
235
238
  )
236
239
 
@@ -287,9 +287,7 @@ const makeFromQueue = <IE>(
287
287
  if (chunk === null) {
288
288
  parser.end()
289
289
  } else {
290
- Chunk.forEach(chunk, function(buf) {
291
- parser.write(buf)
292
- })
290
+ Chunk.forEach(chunk, parser.write)
293
291
  }
294
292
  })
295
293
  )