@effect/platform 0.70.4 → 0.70.6

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
 
@@ -503,18 +492,28 @@ export const fromWebSocket = <RO>(
503
492
  runRaw((data) =>
504
493
  typeof data === "string"
505
494
  ? handler(encoder.encode(data))
506
- : handler(data)
495
+ : data instanceof Uint8Array
496
+ ? handler(data)
497
+ : handler(new Uint8Array(data))
507
498
  )
508
499
 
509
- const write = (chunk: Uint8Array | string | CloseEvent) => sendQueue.offer(chunk)
500
+ const write = (chunk: Uint8Array | string | CloseEvent) =>
501
+ latch.whenOpen(Effect.sync(() => {
502
+ const ws = currentWS!
503
+ if (isCloseEvent(chunk)) {
504
+ ws.close(chunk.code, chunk.reason)
505
+ } else {
506
+ ws.send(chunk)
507
+ }
508
+ }))
510
509
  const writer = Effect.succeed(write)
511
510
 
512
- return Socket.of({
511
+ return Effect.succeed(Socket.of({
513
512
  [TypeId]: TypeId,
514
513
  run,
515
514
  runRaw,
516
515
  writer
517
- })
516
+ }))
518
517
  })
519
518
 
520
519
  /**
@@ -576,77 +575,60 @@ export interface InputTransformStream {
576
575
  export const fromTransformStream = <R>(acquire: Effect.Effect<InputTransformStream, SocketError, R>, options?: {
577
576
  readonly closeCodeIsError?: (code: number) => boolean
578
577
  }): 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
- })
578
+ Effect.withFiberRuntime((fiber) => {
579
+ const latch = Effect.unsafeMakeLatch(false)
580
+ let currentStream: {
581
+ readonly stream: InputTransformStream
582
+ readonly fiberSet: FiberSet.FiberSet<any, any>
583
+ } | undefined
585
584
  const acquireContext = fiber.currentContext as Context.Context<R>
586
585
  const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError
587
586
  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
- )
587
+ Effect.scopedWith((scope) =>
588
+ Effect.gen(function*() {
589
+ const stream = yield* Scope.extend(acquire, scope)
590
+ const reader = stream.readable.getReader()
591
+ yield* Scope.addFinalizer(scope, Effect.promise(() => reader.cancel()))
592
+ const fiberSet = yield* FiberSet.make<any, E | SocketError>().pipe(
593
+ Scope.extend(scope)
594
+ )
595
+ const runFork = yield* FiberSet.runtime(fiberSet)<R>()
596
+
597
+ yield* Effect.tryPromise({
598
+ try: async () => {
599
+ while (true) {
600
+ const { done, value } = await reader.read()
601
+ if (done) {
602
+ throw new SocketCloseError({ reason: "Close", code: 1000 })
603
+ }
604
+ const result = handler(value)
605
+ if (Effect.isEffect(result)) {
606
+ runFork(result)
607
+ }
608
+ }
609
+ },
610
+ catch: (cause) => isSocketError(cause) ? cause : new SocketGenericError({ reason: "Read", cause })
611
+ }).pipe(
612
+ FiberSet.run(fiberSet)
613
+ )
626
614
 
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
- )
615
+ currentStream = { stream, fiberSet }
616
+ yield* latch.open
640
617
 
641
- return yield* FiberSet.join(fiberSet).pipe(
642
- Effect.catchIf(
643
- SocketCloseError.isClean((_) => !closeCodeIsError(_)),
644
- (_) => Effect.void
618
+ return yield* FiberSet.join(fiberSet).pipe(
619
+ Effect.catchIf(
620
+ SocketCloseError.isClean((_) => !closeCodeIsError(_)),
621
+ (_) => Effect.void
622
+ )
645
623
  )
646
- )
647
- }).pipe(
648
- Effect.mapInputContext((input: Context.Context<R | Scope.Scope>) => Context.merge(acquireContext, input)),
649
- Effect.scoped,
624
+ })
625
+ ).pipe(
626
+ (_) => _,
627
+ Effect.mapInputContext((input: Context.Context<R>) => Context.merge(acquireContext, input)),
628
+ Effect.ensuring(Effect.sync(() => {
629
+ latch.unsafeClose()
630
+ currentStream = undefined
631
+ })),
650
632
  Effect.interruptible
651
633
  )
652
634
 
@@ -658,16 +640,39 @@ export const fromTransformStream = <R>(acquire: Effect.Effect<InputTransformStre
658
640
  : handler(data)
659
641
  )
660
642
 
661
- const write = (chunk: Uint8Array | string | CloseEvent) => sendQueue.offer(chunk)
643
+ const writers = new WeakMap<InputTransformStream, WritableStreamDefaultWriter<Uint8Array>>()
644
+ const getWriter = (stream: InputTransformStream) => {
645
+ let writer = writers.get(stream)
646
+ if (!writer) {
647
+ writer = stream.writable.getWriter()
648
+ writers.set(stream, writer)
649
+ }
650
+ return writer
651
+ }
652
+ const write = (chunk: Uint8Array | string | CloseEvent) =>
653
+ latch.whenOpen(Effect.suspend(() => {
654
+ const { fiberSet, stream } = currentStream!
655
+ if (isCloseEvent(chunk)) {
656
+ return Deferred.fail(
657
+ fiberSet.deferred,
658
+ new SocketCloseError({ reason: "Close", code: chunk.code, closeReason: chunk.reason })
659
+ )
660
+ }
661
+ return Effect.promise(() => getWriter(stream).write(typeof chunk === "string" ? encoder.encode(chunk) : chunk))
662
+ }))
662
663
  const writer = Effect.acquireRelease(
663
664
  Effect.succeed(write),
664
- () => sendQueue.end
665
+ () =>
666
+ Effect.promise(async () => {
667
+ if (!currentStream) return
668
+ await getWriter(currentStream.stream).close()
669
+ })
665
670
  )
666
671
 
667
- return Socket.of({
672
+ return Effect.succeed(Socket.of({
668
673
  [TypeId]: TypeId,
669
674
  run,
670
675
  runRaw,
671
676
  writer
672
- })
677
+ }))
673
678
  })