@effect/platform 0.70.4 → 0.70.5

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.
package/src/Socket.ts CHANGED
@@ -9,14 +9,12 @@ import type { DurationInput } from "effect/Duration"
9
9
  import * as Effect from "effect/Effect"
10
10
  import * as ExecutionStrategy from "effect/ExecutionStrategy"
11
11
  import * as Exit from "effect/Exit"
12
- import * as Fiber from "effect/Fiber"
13
12
  import * as FiberRef from "effect/FiberRef"
14
13
  import * as FiberSet from "effect/FiberSet"
15
14
  import { dual } from "effect/Function"
16
15
  import { globalValue } from "effect/GlobalValue"
17
16
  import * as Layer from "effect/Layer"
18
17
  import * as Mailbox from "effect/Mailbox"
19
- import * as Option from "effect/Option"
20
18
  import * as Predicate from "effect/Predicate"
21
19
  import * as Scope from "effect/Scope"
22
20
  import type * as AsyncProducer from "effect/SingleProducerAsyncInput"
@@ -61,7 +59,7 @@ export interface Socket {
61
59
  handler: (_: string | Uint8Array) => Effect.Effect<_, E, R> | void
62
60
  ) => Effect.Effect<void, SocketError | E, R>
63
61
  readonly writer: Effect.Effect<
64
- (chunk: Uint8Array | string | CloseEvent) => Effect.Effect<boolean>,
62
+ (chunk: Uint8Array | string | CloseEvent) => Effect.Effect<void, SocketError>,
65
63
  never,
66
64
  Scope.Scope
67
65
  >
@@ -195,11 +193,16 @@ export const toChannelMap = <IE, A>(
195
193
  const mailbox = yield* Mailbox.make<A, SocketError | IE>()
196
194
  const writeScope = yield* Scope.fork(scope, ExecutionStrategy.sequential)
197
195
  const write = yield* Scope.extend(self.writer, writeScope)
196
+ function* emit(chunk: Chunk.Chunk<Uint8Array | string | CloseEvent>) {
197
+ for (const data of chunk) {
198
+ yield* write(data)
199
+ }
200
+ }
198
201
  const input: AsyncProducer.AsyncInputProducer<IE, Chunk.Chunk<Uint8Array | string | CloseEvent>, unknown> = {
199
202
  awaitRead: () => Effect.void,
200
203
  emit(chunk) {
201
204
  return Effect.catchAllCause(
202
- Effect.forEach(chunk, write, { discard: true }),
205
+ Effect.gen(() => emit(chunk)),
203
206
  (cause) => mailbox.failCause(cause)
204
207
  )
205
208
  },
@@ -395,106 +398,92 @@ export const fromWebSocket = <RO>(
395
398
  readonly openTimeout?: DurationInput
396
399
  }
397
400
  ): Effect.Effect<Socket, never, Exclude<RO, Scope.Scope>> =>
398
- Effect.gen(function*() {
399
- const fiber = Option.getOrThrow(Fiber.getCurrentFiber())
400
- const sendQueue = yield* Mailbox.make<Uint8Array | string | CloseEvent>({
401
- capacity: fiber.getFiberRef(currentSendQueueCapacity),
402
- strategy: "dropping"
403
- })
401
+ Effect.withFiberRuntime((fiber) => {
402
+ let currentWS: globalThis.WebSocket | undefined
403
+ const latch = Effect.unsafeMakeLatch(false)
404
404
  const acquireContext = fiber.currentContext as Context.Context<RO>
405
405
  const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError
406
406
 
407
407
  const runRaw = <_, E, R>(handler: (_: string | Uint8Array) => Effect.Effect<_, E, R> | void) =>
408
- Effect.gen(function*() {
409
- const fiberSet = yield* FiberSet.make<any, E | SocketError>()
410
- const ws = yield* acquire
411
- const run = yield* Effect.provideService(FiberSet.runtime(fiberSet)<R>(), WebSocket, ws)
412
- let open = false
413
-
414
- function onMessage(event: MessageEvent) {
415
- if (event.data instanceof Blob) {
416
- return Effect.promise(() => event.data.arrayBuffer() as Promise<ArrayBuffer>).pipe(
417
- Effect.andThen((buffer) => handler(new Uint8Array(buffer))),
418
- run
419
- )
420
- }
421
- const result = handler(event.data)
422
- if (Effect.isEffect(result)) {
423
- run(result)
424
- }
425
- }
426
- function onError(cause: Event) {
427
- ws.removeEventListener("message", onMessage)
428
- ws.removeEventListener("close", onClose)
429
- Deferred.unsafeDone(
430
- fiberSet.deferred,
431
- Effect.fail(new SocketGenericError({ reason: open ? "Read" : "Open", cause }))
408
+ Effect.scopedWith((scope) =>
409
+ Effect.gen(function*() {
410
+ const fiberSet = yield* FiberSet.make<any, E | SocketError>().pipe(
411
+ Scope.extend(scope)
432
412
  )
433
- }
434
- function onClose(event: globalThis.CloseEvent) {
435
- ws.removeEventListener("message", onMessage)
436
- ws.removeEventListener("error", onError)
437
- Deferred.unsafeDone(
438
- fiberSet.deferred,
439
- Effect.fail(
440
- new SocketCloseError({
441
- reason: "Close",
442
- code: event.code,
443
- closeReason: event.reason
444
- })
413
+ const ws = yield* Scope.extend(acquire, scope)
414
+ const run = yield* Effect.provideService(FiberSet.runtime(fiberSet)<R>(), WebSocket, ws)
415
+ let open = false
416
+
417
+ function onMessage(event: MessageEvent) {
418
+ if (event.data instanceof Blob) {
419
+ return Effect.promise(() => event.data.arrayBuffer() as Promise<ArrayBuffer>).pipe(
420
+ Effect.andThen((buffer) => handler(new Uint8Array(buffer))),
421
+ run
422
+ )
423
+ }
424
+ const result = handler(event.data)
425
+ if (Effect.isEffect(result)) {
426
+ run(result)
427
+ }
428
+ }
429
+ function onError(cause: Event) {
430
+ ws.removeEventListener("message", onMessage)
431
+ ws.removeEventListener("close", onClose)
432
+ Deferred.unsafeDone(
433
+ fiberSet.deferred,
434
+ Effect.fail(new SocketGenericError({ reason: open ? "Read" : "Open", cause }))
445
435
  )
446
- )
447
- }
448
-
449
- ws.addEventListener("close", onClose, { once: true })
450
- ws.addEventListener("error", onError, { once: true })
451
- ws.addEventListener("message", onMessage)
452
-
453
- if (ws.readyState !== 1) {
454
- const openDeferred = Deferred.unsafeMake<void>(fiber.id())
455
- ws.addEventListener("open", () => {
456
- open = true
457
- Deferred.unsafeDone(openDeferred, Effect.void)
458
- }, { once: true })
459
- yield* Deferred.await(openDeferred).pipe(
460
- Effect.timeoutFail({
461
- duration: options?.openTimeout ?? 10000,
462
- onTimeout: () => new SocketGenericError({ reason: "OpenTimeout", cause: "timeout waiting for \"open\"" })
463
- }),
464
- Effect.raceFirst(FiberSet.join(fiberSet))
465
- )
466
- }
467
- open = true
468
- yield* sendQueue.take.pipe(
469
- Effect.tap((chunk) => {
470
- if (isCloseEvent(chunk)) {
471
- ws.close(chunk.code, chunk.reason)
472
- return Effect.fail(
436
+ }
437
+ function onClose(event: globalThis.CloseEvent) {
438
+ ws.removeEventListener("message", onMessage)
439
+ ws.removeEventListener("error", onError)
440
+ Deferred.unsafeDone(
441
+ fiberSet.deferred,
442
+ Effect.fail(
473
443
  new SocketCloseError({
474
444
  reason: "Close",
475
- code: chunk.code,
476
- closeReason: chunk.reason
445
+ code: event.code,
446
+ closeReason: event.reason
477
447
  })
478
448
  )
479
- }
480
- return Effect.try({
481
- try: () => ws.send(chunk),
482
- catch: (cause) => new SocketGenericError({ reason: "Write", cause })
483
- })
484
- }),
485
- Effect.forever,
486
- Effect.catchTag("NoSuchElementException", () => Effect.void),
487
- FiberSet.run(fiberSet)
488
- )
489
- return yield* FiberSet.join(fiberSet).pipe(
490
- Effect.catchIf(
491
- SocketCloseError.isClean((_) => !closeCodeIsError(_)),
492
- (_) => Effect.void
449
+ )
450
+ }
451
+
452
+ ws.addEventListener("close", onClose, { once: true })
453
+ ws.addEventListener("error", onError, { once: true })
454
+ ws.addEventListener("message", onMessage)
455
+
456
+ if (ws.readyState !== 1) {
457
+ const openDeferred = Deferred.unsafeMake<void>(fiber.id())
458
+ ws.addEventListener("open", () => {
459
+ open = true
460
+ Deferred.unsafeDone(openDeferred, Effect.void)
461
+ }, { once: true })
462
+ yield* Deferred.await(openDeferred).pipe(
463
+ Effect.timeoutFail({
464
+ duration: options?.openTimeout ?? 10000,
465
+ onTimeout: () =>
466
+ new SocketGenericError({ reason: "OpenTimeout", cause: "timeout waiting for \"open\"" })
467
+ }),
468
+ Effect.raceFirst(FiberSet.join(fiberSet))
469
+ )
470
+ }
471
+ open = true
472
+ currentWS = ws
473
+ yield* latch.open
474
+ return yield* FiberSet.join(fiberSet).pipe(
475
+ Effect.catchIf(
476
+ SocketCloseError.isClean((_) => !closeCodeIsError(_)),
477
+ (_) => Effect.void
478
+ )
493
479
  )
494
- )
495
- }).pipe(
496
- Effect.mapInputContext((input: Context.Context<R | Scope.Scope>) => Context.merge(acquireContext, input)),
497
- Effect.scoped,
480
+ })
481
+ ).pipe(
482
+ Effect.mapInputContext((input: Context.Context<R>) => Context.merge(acquireContext, input)),
483
+ Effect.ensuring(Effect.sync(() => {
484
+ latch.unsafeClose()
485
+ currentWS = undefined
486
+ })),
498
487
  Effect.interruptible
499
488
  )
500
489
 
@@ -506,15 +495,23 @@ export const fromWebSocket = <RO>(
506
495
  : handler(data)
507
496
  )
508
497
 
509
- const write = (chunk: Uint8Array | string | CloseEvent) => sendQueue.offer(chunk)
498
+ const write = (chunk: Uint8Array | string | CloseEvent) =>
499
+ latch.whenOpen(Effect.sync(() => {
500
+ const ws = currentWS!
501
+ if (isCloseEvent(chunk)) {
502
+ ws.close(chunk.code, chunk.reason)
503
+ } else {
504
+ ws.send(chunk)
505
+ }
506
+ }))
510
507
  const writer = Effect.succeed(write)
511
508
 
512
- return Socket.of({
509
+ return Effect.succeed(Socket.of({
513
510
  [TypeId]: TypeId,
514
511
  run,
515
512
  runRaw,
516
513
  writer
517
- })
514
+ }))
518
515
  })
519
516
 
520
517
  /**
@@ -576,77 +573,60 @@ export interface InputTransformStream {
576
573
  export const fromTransformStream = <R>(acquire: Effect.Effect<InputTransformStream, SocketError, R>, options?: {
577
574
  readonly closeCodeIsError?: (code: number) => boolean
578
575
  }): Effect.Effect<Socket, never, Exclude<R, Scope.Scope>> =>
579
- Effect.gen(function*() {
580
- const fiber = Option.getOrThrow(Fiber.getCurrentFiber())
581
- const sendQueue = yield* Mailbox.make<Uint8Array | string | CloseEvent>({
582
- capacity: fiber.getFiberRef(currentSendQueueCapacity),
583
- strategy: "dropping"
584
- })
576
+ Effect.withFiberRuntime((fiber) => {
577
+ const latch = Effect.unsafeMakeLatch(false)
578
+ let currentStream: {
579
+ readonly stream: InputTransformStream
580
+ readonly fiberSet: FiberSet.FiberSet<any, any>
581
+ } | undefined
585
582
  const acquireContext = fiber.currentContext as Context.Context<R>
586
583
  const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError
587
584
  const runRaw = <_, E, R>(handler: (_: string | Uint8Array) => Effect.Effect<_, E, R> | void) =>
588
- Effect.gen(function*() {
589
- const stream = yield* acquire
590
- const reader = yield* Effect.acquireRelease(
591
- Effect.sync(() => stream.readable.getReader()),
592
- (reader) =>
593
- Effect.promise(() => reader.cancel()).pipe(
594
- Effect.tap(() => {
595
- reader.releaseLock()
596
- })
597
- )
598
- )
599
- const writer = yield* Effect.acquireRelease(
600
- Effect.sync(() => stream.writable.getWriter()),
601
- (reader) => Effect.sync(() => reader.releaseLock())
602
- )
603
- const fiberSet = yield* FiberSet.make<any, E | SocketError>()
604
- const encoder = new TextEncoder()
605
- yield* sendQueue.take.pipe(
606
- Effect.tap((chunk) => {
607
- if (isCloseEvent(chunk)) {
608
- return Effect.fail(
609
- new SocketCloseError({
610
- reason: "Close",
611
- code: chunk.code,
612
- closeReason: chunk.reason
613
- })
614
- )
615
- }
616
- return Effect.tryPromise({
617
- try: () => writer.write(typeof chunk === "string" ? encoder.encode(chunk) : chunk),
618
- catch: (cause) => new SocketGenericError({ reason: "Write", cause })
619
- })
620
- }),
621
- Effect.forever,
622
- Effect.catchTag("NoSuchElementException", () => Effect.void),
623
- Effect.ensuring(Effect.promise(() => writer.close())),
624
- FiberSet.run(fiberSet)
625
- )
585
+ Effect.scopedWith((scope) =>
586
+ Effect.gen(function*() {
587
+ const stream = yield* Scope.extend(acquire, scope)
588
+ const reader = stream.readable.getReader()
589
+ yield* Scope.addFinalizer(scope, Effect.promise(() => reader.cancel()))
590
+ const fiberSet = yield* FiberSet.make<any, E | SocketError>().pipe(
591
+ Scope.extend(scope)
592
+ )
593
+ const runFork = yield* FiberSet.runtime(fiberSet)<R>()
594
+
595
+ yield* Effect.tryPromise({
596
+ try: async () => {
597
+ while (true) {
598
+ const { done, value } = await reader.read()
599
+ if (done) {
600
+ throw new SocketCloseError({ reason: "Close", code: 1000 })
601
+ }
602
+ const result = handler(value)
603
+ if (Effect.isEffect(result)) {
604
+ runFork(result)
605
+ }
606
+ }
607
+ },
608
+ catch: (cause) => isSocketError(cause) ? cause : new SocketGenericError({ reason: "Read", cause })
609
+ }).pipe(
610
+ FiberSet.run(fiberSet)
611
+ )
626
612
 
627
- yield* Effect.tryPromise({
628
- try: () => reader.read(),
629
- catch: (cause) => new SocketGenericError({ reason: "Read", cause })
630
- }).pipe(
631
- Effect.tap((result) => {
632
- if (result.done) {
633
- return Effect.fail(new SocketCloseError({ reason: "Close", code: 1000 }))
634
- }
635
- return handler(result.value)
636
- }),
637
- Effect.forever,
638
- FiberSet.run(fiberSet)
639
- )
613
+ currentStream = { stream, fiberSet }
614
+ yield* latch.open
640
615
 
641
- return yield* FiberSet.join(fiberSet).pipe(
642
- Effect.catchIf(
643
- SocketCloseError.isClean((_) => !closeCodeIsError(_)),
644
- (_) => Effect.void
616
+ return yield* FiberSet.join(fiberSet).pipe(
617
+ Effect.catchIf(
618
+ SocketCloseError.isClean((_) => !closeCodeIsError(_)),
619
+ (_) => Effect.void
620
+ )
645
621
  )
646
- )
647
- }).pipe(
648
- Effect.mapInputContext((input: Context.Context<R | Scope.Scope>) => Context.merge(acquireContext, input)),
649
- Effect.scoped,
622
+ })
623
+ ).pipe(
624
+ (_) => _,
625
+ Effect.mapInputContext((input: Context.Context<R>) => Context.merge(acquireContext, input)),
626
+ Effect.ensuring(Effect.sync(() => {
627
+ latch.unsafeClose()
628
+ currentStream = undefined
629
+ })),
650
630
  Effect.interruptible
651
631
  )
652
632
 
@@ -658,16 +638,39 @@ export const fromTransformStream = <R>(acquire: Effect.Effect<InputTransformStre
658
638
  : handler(data)
659
639
  )
660
640
 
661
- const write = (chunk: Uint8Array | string | CloseEvent) => sendQueue.offer(chunk)
641
+ const writers = new WeakMap<InputTransformStream, WritableStreamDefaultWriter<Uint8Array>>()
642
+ const getWriter = (stream: InputTransformStream) => {
643
+ let writer = writers.get(stream)
644
+ if (!writer) {
645
+ writer = stream.writable.getWriter()
646
+ writers.set(stream, writer)
647
+ }
648
+ return writer
649
+ }
650
+ const write = (chunk: Uint8Array | string | CloseEvent) =>
651
+ latch.whenOpen(Effect.suspend(() => {
652
+ const { fiberSet, stream } = currentStream!
653
+ if (isCloseEvent(chunk)) {
654
+ return Deferred.fail(
655
+ fiberSet.deferred,
656
+ new SocketCloseError({ reason: "Close", code: chunk.code, closeReason: chunk.reason })
657
+ )
658
+ }
659
+ return Effect.promise(() => getWriter(stream).write(typeof chunk === "string" ? encoder.encode(chunk) : chunk))
660
+ }))
662
661
  const writer = Effect.acquireRelease(
663
662
  Effect.succeed(write),
664
- () => sendQueue.end
663
+ () =>
664
+ Effect.promise(async () => {
665
+ if (!currentStream) return
666
+ await getWriter(currentStream.stream).close()
667
+ })
665
668
  )
666
669
 
667
- return Socket.of({
670
+ return Effect.succeed(Socket.of({
668
671
  [TypeId]: TypeId,
669
672
  run,
670
673
  runRaw,
671
674
  writer
672
- })
675
+ }))
673
676
  })