@livestore/utils 0.4.0-dev.3 → 0.4.0-dev.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.
Files changed (61) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/NoopTracer.d.ts.map +1 -1
  3. package/dist/NoopTracer.js +1 -0
  4. package/dist/NoopTracer.js.map +1 -1
  5. package/dist/effect/Error.d.ts +1 -1
  6. package/dist/effect/Error.js.map +1 -1
  7. package/dist/effect/Logger.d.ts +4 -1
  8. package/dist/effect/Logger.d.ts.map +1 -1
  9. package/dist/effect/Logger.js +12 -3
  10. package/dist/effect/Logger.js.map +1 -1
  11. package/dist/effect/OtelTracer.d.ts +5 -0
  12. package/dist/effect/OtelTracer.d.ts.map +1 -0
  13. package/dist/effect/OtelTracer.js +8 -0
  14. package/dist/effect/OtelTracer.js.map +1 -0
  15. package/dist/effect/RpcClient.d.ts +32 -0
  16. package/dist/effect/RpcClient.d.ts.map +1 -0
  17. package/dist/effect/RpcClient.js +141 -0
  18. package/dist/effect/RpcClient.js.map +1 -0
  19. package/dist/effect/Schema/index.d.ts +0 -1
  20. package/dist/effect/Schema/index.d.ts.map +1 -1
  21. package/dist/effect/Schema/index.js +0 -1
  22. package/dist/effect/Schema/index.js.map +1 -1
  23. package/dist/effect/Stream.d.ts +73 -2
  24. package/dist/effect/Stream.d.ts.map +1 -1
  25. package/dist/effect/Stream.js +68 -1
  26. package/dist/effect/Stream.js.map +1 -1
  27. package/dist/effect/Stream.test.d.ts +2 -0
  28. package/dist/effect/Stream.test.d.ts.map +1 -0
  29. package/dist/effect/Stream.test.js +84 -0
  30. package/dist/effect/Stream.test.js.map +1 -0
  31. package/dist/effect/SubscriptionRef.d.ts +2 -2
  32. package/dist/effect/SubscriptionRef.d.ts.map +1 -1
  33. package/dist/effect/SubscriptionRef.js +6 -1
  34. package/dist/effect/SubscriptionRef.js.map +1 -1
  35. package/dist/effect/WebSocket.js +1 -1
  36. package/dist/effect/WebSocket.js.map +1 -1
  37. package/dist/effect/index.d.ts +9 -5
  38. package/dist/effect/index.d.ts.map +1 -1
  39. package/dist/effect/index.js +10 -7
  40. package/dist/effect/index.js.map +1 -1
  41. package/dist/global.d.ts +1 -0
  42. package/dist/global.d.ts.map +1 -1
  43. package/dist/global.js.map +1 -1
  44. package/dist/node/mod.d.ts +1 -1
  45. package/dist/node/mod.d.ts.map +1 -1
  46. package/dist/node/mod.js.map +1 -1
  47. package/package.json +29 -28
  48. package/src/NoopTracer.ts +1 -0
  49. package/src/effect/Error.ts +1 -1
  50. package/src/effect/Logger.ts +14 -4
  51. package/src/effect/OtelTracer.ts +11 -0
  52. package/src/effect/RpcClient.ts +204 -0
  53. package/src/effect/Schema/index.ts +0 -1
  54. package/src/effect/Stream.test.ts +127 -0
  55. package/src/effect/Stream.ts +111 -2
  56. package/src/effect/SubscriptionRef.ts +14 -2
  57. package/src/effect/WebSocket.ts +1 -1
  58. package/src/effect/index.ts +15 -5
  59. package/src/global.ts +1 -0
  60. package/src/node/mod.ts +1 -1
  61. package/src/effect/Schema/msgpack.ts +0 -8
@@ -0,0 +1,204 @@
1
+ export * from '@effect/rpc/RpcClient'
2
+
3
+ import { Socket } from '@effect/platform'
4
+ import { RpcClient, RpcClientError, RpcSerialization } from '@effect/rpc'
5
+ import { Protocol } from '@effect/rpc/RpcClient'
6
+ import { constPing, type FromServerEncoded } from '@effect/rpc/RpcMessage'
7
+ import { Cause, Deferred, Effect, Layer, Option, Schedule, type Scope } from 'effect'
8
+ import { constVoid, identity } from 'effect/Function'
9
+ import * as SubscriptionRef from './SubscriptionRef.ts'
10
+
11
+ // This is based on `makeProtocolSocket` / `layerProtocolSocket` from `@effect/rpc` in order to:
12
+ // - Add a `isConnected` subscription ref to track the connection state
13
+ // - Add a ping schedule to the socket
14
+ // - Add a retry schedule to the socket
15
+
16
+ export const layerProtocolSocketWithIsConnected = (options: {
17
+ readonly url: string
18
+ readonly retryTransientErrors?: Schedule.Schedule<unknown> | undefined
19
+ readonly isConnected: SubscriptionRef.SubscriptionRef<boolean>
20
+ readonly pingSchedule?: Schedule.Schedule<unknown> | undefined
21
+ }): Layer.Layer<Protocol, never, RpcSerialization.RpcSerialization | Socket.Socket> =>
22
+ Layer.scoped(Protocol, makeProtocolSocketWithIsConnected(options))
23
+
24
+ export const makeProtocolSocketWithIsConnected = (options: {
25
+ readonly url: string
26
+ readonly retryTransientErrors?: Schedule.Schedule<unknown> | undefined
27
+ // CHANGED: add isConnected subscription ref
28
+ readonly isConnected: SubscriptionRef.SubscriptionRef<boolean>
29
+ // CHANGED: add ping schedule
30
+ readonly pingSchedule?: Schedule.Schedule<unknown> | undefined
31
+ }): Effect.Effect<Protocol['Type'], never, Scope.Scope | RpcSerialization.RpcSerialization | Socket.Socket> =>
32
+ Protocol.make(
33
+ Effect.fnUntraced(function* (writeResponse) {
34
+ const socket = yield* Socket.Socket
35
+ const serialization = yield* RpcSerialization.RpcSerialization
36
+
37
+ const write = yield* socket.writer
38
+ const parser = serialization.unsafeMake()
39
+
40
+ const pinger = yield* makePinger(write(parser.encode(constPing)!), options?.pingSchedule)
41
+
42
+ yield* Effect.suspend(() => {
43
+ // CHANGED: don't reset parser on every message
44
+ // parser = serialization.unsafeMake()
45
+ pinger.reset()
46
+ return socket
47
+ .runRaw((message) => {
48
+ try {
49
+ const responses = parser.decode(message) as Array<FromServerEncoded>
50
+ if (responses.length === 0) return
51
+ let i = 0
52
+ return Effect.whileLoop({
53
+ while: () => i < responses.length,
54
+ body: () => {
55
+ const response = responses[i++]!
56
+ if (response._tag === 'Pong') {
57
+ pinger.onPong()
58
+ }
59
+ return writeResponse(response).pipe(
60
+ // CHANGED: set isConnected to true on pong
61
+ Effect.tap(
62
+ Effect.fn(function* () {
63
+ if (options?.isConnected !== undefined) {
64
+ yield* SubscriptionRef.set(options.isConnected, true)
65
+ }
66
+ }),
67
+ ),
68
+ )
69
+ },
70
+ step: constVoid,
71
+ })
72
+ } catch (defect) {
73
+ return writeResponse({
74
+ _tag: 'ClientProtocolError',
75
+ error: new RpcClientError.RpcClientError({
76
+ reason: 'Protocol',
77
+ message: 'Error decoding message',
78
+ cause: Cause.fail(defect),
79
+ }),
80
+ })
81
+ }
82
+ })
83
+ .pipe(
84
+ Effect.raceFirst(
85
+ Effect.zipRight(
86
+ pinger.timeout,
87
+ Effect.fail(
88
+ new Socket.SocketGenericError({
89
+ reason: 'OpenTimeout',
90
+ cause: new Error('ping timeout'),
91
+ }),
92
+ ),
93
+ ),
94
+ ),
95
+ )
96
+ }).pipe(
97
+ Effect.zipRight(
98
+ Effect.fail(
99
+ new Socket.SocketCloseError({
100
+ reason: 'Close',
101
+ code: 1000,
102
+ closeReason: 'Closing connection',
103
+ }),
104
+ ),
105
+ ),
106
+ Effect.tapErrorCause(
107
+ Effect.fn(function* (cause) {
108
+ // CHANGED: set isConnected to false on error
109
+ if (options?.isConnected !== undefined) {
110
+ yield* SubscriptionRef.set(options.isConnected, false)
111
+ }
112
+
113
+ const error = Cause.failureOption(cause)
114
+ if (
115
+ options?.retryTransientErrors &&
116
+ Option.isSome(error) &&
117
+ (error.value.reason === 'Open' || error.value.reason === 'OpenTimeout')
118
+ ) {
119
+ return
120
+ }
121
+ return yield* writeResponse({
122
+ _tag: 'ClientProtocolError',
123
+ error: new RpcClientError.RpcClientError({
124
+ reason: 'Protocol',
125
+ message: 'Error in socket',
126
+ cause: Cause.squash(cause),
127
+ }),
128
+ })
129
+ }),
130
+ ),
131
+ // CHANGED: make configurable via schedule
132
+ options?.retryTransientErrors ? Effect.retry(options.retryTransientErrors) : identity,
133
+ Effect.annotateLogs({
134
+ module: 'RpcClient',
135
+ method: 'makeProtocolSocket',
136
+ }),
137
+ Effect.interruptible,
138
+ Effect.ignore, // Errors are already handled
139
+ Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
140
+ Effect.forkScoped,
141
+ )
142
+
143
+ return {
144
+ send: (request) => {
145
+ const encoded = parser.encode(request)
146
+ if (encoded === undefined) return Effect.void
147
+
148
+ return Effect.orDie(write(encoded))
149
+ },
150
+ supportsAck: true,
151
+ supportsTransferables: false,
152
+ pinger,
153
+ }
154
+ }),
155
+ )
156
+
157
+ export const SocketPinger = Effect.map(RpcClient.Protocol, (protocol) => (protocol as any).pinger as SocketPinger)
158
+
159
+ export type SocketPinger = Effect.Effect.Success<ReturnType<typeof makePinger>>
160
+
161
+ const makePinger = Effect.fnUntraced(function* <A, E, R>(
162
+ writePing: Effect.Effect<A, E, R>,
163
+ pingSchedule: Schedule.Schedule<unknown> = Schedule.spaced(10000).pipe(Schedule.addDelay(() => 5000)),
164
+ ) {
165
+ // CHANGED: add manual ping deferreds
166
+ const manualPingDeferreds = new Set<Deferred.Deferred<void, never>>()
167
+
168
+ let recievedPong = true
169
+ const latch = Effect.unsafeMakeLatch()
170
+ const reset = () => {
171
+ recievedPong = true
172
+ latch.unsafeClose()
173
+ }
174
+ const onPong = () => {
175
+ recievedPong = true
176
+ // CHANGED: mark all manual ping deferreds as done
177
+ for (const deferred of manualPingDeferreds) {
178
+ Deferred.unsafeDone(deferred, Effect.void)
179
+ }
180
+ }
181
+ yield* Effect.suspend(() => {
182
+ // Starting new ping
183
+ if (!recievedPong) return latch.open
184
+ recievedPong = false
185
+ return writePing
186
+ }).pipe(
187
+ // CHANGED: make configurable via schedule
188
+ Effect.schedule(pingSchedule),
189
+ Effect.ignore,
190
+ Effect.forever,
191
+ Effect.interruptible,
192
+ Effect.forkScoped,
193
+ )
194
+
195
+ // CHANGED: add manual ping
196
+ const ping = Effect.gen(function* () {
197
+ const deferred = yield* Deferred.make<void, never>()
198
+ manualPingDeferreds.add(deferred)
199
+ yield* deferred
200
+ manualPingDeferreds.delete(deferred)
201
+ })
202
+
203
+ return { timeout: latch.await, reset, onPong, ping } as const
204
+ })
@@ -8,7 +8,6 @@ import { shouldNeverHappen } from '../../mod.ts'
8
8
 
9
9
  export * from 'effect/Schema'
10
10
  export * from './debug-diff.ts'
11
- export * from './msgpack.ts'
12
11
 
13
12
  // NOTE this is a temporary workaround until Effect schema has a better way to hash schemas
14
13
  // https://github.com/Effect-TS/effect/issues/2719
@@ -0,0 +1,127 @@
1
+ import { Effect, Option, Stream } from 'effect'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { concatWithLastElement, runCollectReadonlyArray } from './Stream.ts'
4
+
5
+ describe('concatWithLastElement', () => {
6
+ it('should concatenate streams with access to last element of first stream', async () => {
7
+ const stream1 = Stream.make(1, 2, 3)
8
+ const result = concatWithLastElement(stream1, (lastElement) =>
9
+ lastElement.pipe(
10
+ Option.match({
11
+ onNone: () => Stream.make('no-previous'),
12
+ onSome: (last) => Stream.make(`last-was-${last}`, 'continuing'),
13
+ }),
14
+ ),
15
+ )
16
+
17
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
18
+ expect(collected).toEqual([1, 2, 3, 'last-was-3', 'continuing'])
19
+ })
20
+
21
+ it('should handle empty first stream', async () => {
22
+ const stream1 = Stream.empty
23
+ const result = concatWithLastElement(stream1, (lastElement) =>
24
+ lastElement.pipe(
25
+ Option.match({
26
+ onNone: () => Stream.make('no-previous-element'),
27
+ onSome: (last) => Stream.make(`last-was-${last}`),
28
+ }),
29
+ ),
30
+ )
31
+
32
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
33
+ expect(collected).toEqual(['no-previous-element'])
34
+ })
35
+
36
+ it('should handle single element first stream', async () => {
37
+ const stream1 = Stream.make('single')
38
+ const result = concatWithLastElement(stream1, (lastElement) =>
39
+ lastElement.pipe(
40
+ Option.match({
41
+ onNone: () => Stream.make('unexpected'),
42
+ onSome: (last) => Stream.make(`after-${last}`),
43
+ }),
44
+ ),
45
+ )
46
+
47
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
48
+ expect(collected).toEqual(['single', 'after-single'])
49
+ })
50
+
51
+ it('should handle empty second stream', async () => {
52
+ const stream1 = Stream.make(1, 2, 3)
53
+ const result = concatWithLastElement(stream1, () => Stream.empty)
54
+
55
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
56
+ expect(collected).toEqual([1, 2, 3])
57
+ })
58
+
59
+ it('should preserve error handling from first stream', async () => {
60
+ const stream1 = Stream.fail('first-error')
61
+ const result = concatWithLastElement(stream1, () => Stream.make('should-not-reach'))
62
+
63
+ const outcome = await Effect.runPromise(Effect.either(runCollectReadonlyArray(result)))
64
+ expect(outcome._tag).toBe('Left')
65
+ if (outcome._tag === 'Left') {
66
+ expect(outcome.left).toBe('first-error')
67
+ }
68
+ })
69
+
70
+ it('should preserve error handling from second stream', async () => {
71
+ const stream1 = Stream.make(1, 2)
72
+ const result = concatWithLastElement(stream1, () => Stream.fail('second-error'))
73
+
74
+ const outcome = await Effect.runPromise(Effect.either(runCollectReadonlyArray(result)))
75
+ expect(outcome._tag).toBe('Left')
76
+ if (outcome._tag === 'Left') {
77
+ expect(outcome.left).toBe('second-error')
78
+ }
79
+ })
80
+
81
+ it('should work with different types in streams', async () => {
82
+ const stream1 = Stream.make(1, 2, 3)
83
+ const result = concatWithLastElement(stream1, (lastElement) =>
84
+ lastElement.pipe(
85
+ Option.match({
86
+ onNone: () => Stream.make('no-number') as Stream.Stream<number | string, never, never>,
87
+ onSome: (last) => Stream.make(last * 10, last * 100),
88
+ }),
89
+ ),
90
+ )
91
+
92
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
93
+ expect(collected).toEqual([1, 2, 3, 30, 300])
94
+ })
95
+
96
+ it('should handle async effects in streams', async () => {
97
+ const stream1 = Stream.fromEffect(Effect.succeed('async-value'))
98
+ const result = concatWithLastElement(stream1, (lastElement) =>
99
+ lastElement.pipe(
100
+ Option.match({
101
+ onNone: () => Stream.fromEffect(Effect.succeed('no-async')),
102
+ onSome: (last) => Stream.fromEffect(Effect.succeed(`processed-${last}`)),
103
+ }),
104
+ ),
105
+ )
106
+
107
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
108
+ expect(collected).toEqual(['async-value', 'processed-async-value'])
109
+ })
110
+
111
+ it('should work with dual function - piped style', async () => {
112
+ const stream1 = Stream.make('a', 'b', 'c')
113
+ const result = stream1.pipe(
114
+ concatWithLastElement((lastElement) =>
115
+ lastElement.pipe(
116
+ Option.match({
117
+ onNone: () => Stream.make('no-last'),
118
+ onSome: (last) => Stream.make(`last-${last}`, 'done'),
119
+ }),
120
+ ),
121
+ ),
122
+ )
123
+
124
+ const collected = await Effect.runPromise(runCollectReadonlyArray(result))
125
+ expect(collected).toEqual(['a', 'b', 'c', 'last-c', 'done'])
126
+ })
127
+ })
@@ -1,7 +1,8 @@
1
+ /** biome-ignore-all lint/suspicious/useIterableCallbackReturn: Biome bug */
1
2
  export * from 'effect/Stream'
2
3
 
3
- import type { Chunk } from 'effect'
4
- import { Effect, Option, pipe, Ref, Stream } from 'effect'
4
+ import { type Cause, Chunk, Effect, Option, pipe, Ref, Stream } from 'effect'
5
+ import { dual } from 'effect/Function'
5
6
 
6
7
  export const tapLog = <R, E, A>(stream: Stream.Stream<A, E, R>): Stream.Stream<A, E, R> =>
7
8
  tapChunk<never, never, A, void>(Effect.forEach((_) => Effect.succeed(console.log(_))))(stream)
@@ -61,3 +62,111 @@ export const skipRepeated_ = <R, E, A>(
61
62
  ),
62
63
  ),
63
64
  )
65
+
66
+ /**
67
+ * Returns the first element of the stream or `None` if the stream is empty.
68
+ * It's different than `Stream.runHead` which runs the stream to completion.
69
+ * */
70
+ export const runFirst = <A, E, R>(stream: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<A>, E, R> =>
71
+ stream.pipe(Stream.take(1), Stream.runCollect, Effect.map(Chunk.head))
72
+
73
+ /**
74
+ * Returns the first element of the stream or throws a `NoSuchElementException` if the stream is empty.
75
+ * It's different than `Stream.runHead` which runs the stream to completion.
76
+ * */
77
+ export const runFirstUnsafe = <A, E, R>(
78
+ stream: Stream.Stream<A, E, R>,
79
+ ): Effect.Effect<A, Cause.NoSuchElementException | E, R> => runFirst(stream).pipe(Effect.flatten)
80
+
81
+ export const runCollectReadonlyArray = <A, E, R>(stream: Stream.Stream<A, E, R>): Effect.Effect<readonly A[], E, R> =>
82
+ stream.pipe(Stream.runCollect, Effect.map(Chunk.toReadonlyArray))
83
+
84
+ /**
85
+ * Concatenates two streams where the second stream has access to the last element
86
+ * of the first stream as an `Option`. If the first stream is empty, the callback
87
+ * receives `Option.none()`.
88
+ *
89
+ * @param stream - The first stream to consume
90
+ * @param getStream2 - Function that receives the last element from the first stream
91
+ * and returns the second stream to concatenate
92
+ * @returns A new stream containing all elements from both streams
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * // Direct usage
97
+ * const result = concatWithLastElement(
98
+ * Stream.make(1, 2, 3),
99
+ * lastElement => lastElement.pipe(
100
+ * Option.match({
101
+ * onNone: () => Stream.make('empty'),
102
+ * onSome: last => Stream.make(`last-was-${last}`)
103
+ * })
104
+ * )
105
+ * )
106
+ *
107
+ * // Piped usage
108
+ * const result = Stream.make(1, 2, 3).pipe(
109
+ * concatWithLastElement(lastElement =>
110
+ * Stream.make(lastElement.pipe(Option.getOrElse(() => 0)) * 10)
111
+ * )
112
+ * )
113
+ * ```
114
+ */
115
+ export const concatWithLastElement: {
116
+ <A1, A2, E2, R2>(
117
+ getStream2: (lastElement: Option.Option<A1>) => Stream.Stream<A2, E2, R2>,
118
+ ): <E1, R1>(stream: Stream.Stream<A1, E1, R1>) => Stream.Stream<A1 | A2, E1 | E2, R1 | R2>
119
+ <A1, E1, R1, A2, E2, R2>(
120
+ stream: Stream.Stream<A1, E1, R1>,
121
+ getStream2: (lastElement: Option.Option<A1>) => Stream.Stream<A2, E2, R2>,
122
+ ): Stream.Stream<A1 | A2, E1 | E2, R1 | R2>
123
+ } = dual(
124
+ 2,
125
+ <A1, E1, R1, A2, E2, R2>(
126
+ stream1: Stream.Stream<A1, E1, R1>,
127
+ getStream2: (lastElement: Option.Option<A1>) => Stream.Stream<A2, E2, R2>,
128
+ ): Stream.Stream<A1 | A2, E1 | E2, R1 | R2> =>
129
+ pipe(
130
+ Ref.make<Option.Option<A1>>(Option.none()),
131
+ Stream.fromEffect,
132
+ Stream.flatMap((lastRef) =>
133
+ pipe(
134
+ stream1,
135
+ Stream.tap((value) => Ref.set(lastRef, Option.some(value))),
136
+ Stream.concat(pipe(Ref.get(lastRef), Effect.map(getStream2), Stream.unwrap)),
137
+ ),
138
+ ),
139
+ ),
140
+ )
141
+
142
+ /**
143
+ * Emits a default value if the stream is empty, otherwise passes through all elements.
144
+ * Uses `concatWithLastElement` internally to detect if the stream was empty.
145
+ *
146
+ * @param fallbackValue - The value to emit if the stream is empty
147
+ * @returns A dual function that can be used in pipe or direct call
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * // Direct usage
152
+ * const result = emitIfEmpty(Stream.empty, 'default')
153
+ * // Emits: 'default'
154
+ *
155
+ * // Piped usage
156
+ * const result = Stream.make(1, 2, 3).pipe(emitIfEmpty('fallback'))
157
+ * // Emits: 1, 2, 3
158
+ *
159
+ * const empty = Stream.empty.pipe(emitIfEmpty('fallback'))
160
+ * // Emits: 'fallback'
161
+ * ```
162
+ */
163
+ export const emitIfEmpty: {
164
+ <A>(fallbackValue: A): <E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E, R>
165
+ <A, E, R>(stream: Stream.Stream<A, E, R>, fallbackValue: A): Stream.Stream<A, E, R>
166
+ } = dual(
167
+ 2,
168
+ <A, E, R>(stream: Stream.Stream<A, E, R>, fallbackValue: A): Stream.Stream<A, E, R> =>
169
+ concatWithLastElement(stream, (lastElement) =>
170
+ lastElement._tag === 'None' ? Stream.make(fallbackValue) : Stream.empty,
171
+ ),
172
+ )
@@ -1,5 +1,4 @@
1
- import type { SubscriptionRef } from 'effect'
2
- import { Chunk, Effect, pipe, Stream } from 'effect'
1
+ import { Chunk, Effect, pipe, Stream, SubscriptionRef } from 'effect'
3
2
  import { dual } from 'effect/Function'
4
3
  import type { Predicate, Refinement } from 'effect/Predicate'
5
4
 
@@ -20,3 +19,16 @@ export const waitUntil: {
20
19
  } = dual(2, <A>(sref: SubscriptionRef.SubscriptionRef<A>, predicate: (a: A) => boolean) =>
21
20
  pipe(sref.changes, Stream.filter(predicate), Stream.take(1), Stream.runCollect, Effect.map(Chunk.unsafeHead)),
22
21
  )
22
+
23
+ export const fromStream = <A>(stream: Stream.Stream<A>, initialValue: A) =>
24
+ Effect.gen(function* () {
25
+ const sref = yield* SubscriptionRef.make(initialValue)
26
+
27
+ yield* stream.pipe(
28
+ Stream.tap((a) => SubscriptionRef.set(sref, a)),
29
+ Stream.runDrain,
30
+ Effect.forkScoped,
31
+ )
32
+
33
+ return sref
34
+ })
@@ -88,7 +88,7 @@ export const makeWebSocket = ({
88
88
  socket.close(1000)
89
89
  }
90
90
  } catch (error) {
91
- yield* Effect.die(new WebSocketError({ cause: error }))
91
+ return yield* Effect.die(new WebSocketError({ cause: error }))
92
92
  }
93
93
  }),
94
94
  )
@@ -1,6 +1,7 @@
1
1
  import '../global.ts'
2
2
 
3
- export * as OtelTracer from '@effect/opentelemetry/Tracer'
3
+ export { AiError, AiLanguageModel, AiModel, AiTool, AiToolkit, McpSchema, McpServer } from '@effect/ai'
4
+ export * as Otlp from '@effect/opentelemetry/Otlp'
4
5
  export {
5
6
  Command,
6
7
  CommandExecutor,
@@ -8,6 +9,11 @@ export {
8
9
  FetchHttpClient,
9
10
  FileSystem,
10
11
  Headers,
12
+ HttpApi,
13
+ HttpApiClient,
14
+ HttpApiEndpoint,
15
+ HttpApiGroup,
16
+ HttpApp,
11
17
  HttpClient,
12
18
  HttpClientError,
13
19
  HttpClientRequest,
@@ -18,6 +24,7 @@ export {
18
24
  HttpServerRequest,
19
25
  HttpServerResponse,
20
26
  KeyValueStore,
27
+ MsgPack,
21
28
  Socket,
22
29
  Terminal,
23
30
  Transferable,
@@ -29,7 +36,8 @@ export {
29
36
  export { BrowserWorker, BrowserWorkerRunner } from '@effect/platform-browser'
30
37
  export {
31
38
  Rpc,
32
- RpcClient,
39
+ // RpcClient, // TODO bring back "original" RpcClient from effect/rpc
40
+ RpcClientError,
33
41
  RpcGroup,
34
42
  RpcMessage,
35
43
  RpcMiddleware,
@@ -47,8 +55,8 @@ export {
47
55
  Cause,
48
56
  Channel,
49
57
  Chunk,
50
- // Logger,
51
58
  Config,
59
+ ConfigError,
52
60
  Console,
53
61
  Context,
54
62
  Data,
@@ -107,13 +115,15 @@ export {
107
115
  Tracer,
108
116
  Types,
109
117
  } from 'effect'
110
- export { dual } from 'effect/Function'
118
+ export type { NonEmptyArray } from 'effect/Array'
119
+ export { constVoid, dual } from 'effect/Function'
111
120
  export { TreeFormatter } from 'effect/ParseResult'
112
121
  export type { Serializable, SerializableWithResult } from 'effect/Schema'
113
-
114
122
  export * as SchemaAST from 'effect/SchemaAST'
115
123
  export * as BucketQueue from './BucketQueue.ts'
116
124
  export * as Logger from './Logger.ts'
125
+ export * as OtelTracer from './OtelTracer.ts'
126
+ export * as RpcClient from './RpcClient.ts'
117
127
  export * as Schema from './Schema/index.ts'
118
128
  export * as Stream from './Stream.ts'
119
129
  export * as Subscribable from './Subscribable.ts'
package/src/global.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  declare global {
2
2
  export type TODO<_Reason extends string = 'unknown'> = any
3
+ export type UNUSED<_Reason extends string = 'unknown'> = any
3
4
  }
4
5
 
5
6
  export {}
package/src/node/mod.ts CHANGED
@@ -17,7 +17,7 @@ export * as ChildProcessWorker from './ChildProcessRunner/ChildProcessWorker.ts'
17
17
 
18
18
  // export const OtelLiveHttp = (args: any): Layer.Layer<never> => Layer.empty
19
19
 
20
- export const getFreePort = Effect.async<number, UnknownError>((cb, signal) => {
20
+ export const getFreePort: Effect.Effect<number, UnknownError> = Effect.async<number, UnknownError>((cb, signal) => {
21
21
  const server = http.createServer()
22
22
 
23
23
  signal.addEventListener('abort', () => {
@@ -1,8 +0,0 @@
1
- import { Schema } from 'effect'
2
- import * as msgpack from 'msgpackr'
3
-
4
- export const MsgPack = <A, I>(schema: Schema.Schema<A, I>) =>
5
- Schema.transform(Schema.Uint8ArrayFromSelf, schema, {
6
- encode: (decoded) => msgpack.pack(decoded),
7
- decode: (encodedBytes) => msgpack.unpack(encodedBytes),
8
- })