@livestore/utils 0.4.0-dev.1 → 0.4.0-dev.10

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 (102) 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/Effect.d.ts +9 -3
  6. package/dist/effect/Effect.d.ts.map +1 -1
  7. package/dist/effect/Effect.js +4 -2
  8. package/dist/effect/Effect.js.map +1 -1
  9. package/dist/effect/Error.d.ts +1 -1
  10. package/dist/effect/Error.js.map +1 -1
  11. package/dist/effect/Logger.d.ts +4 -1
  12. package/dist/effect/Logger.d.ts.map +1 -1
  13. package/dist/effect/Logger.js +12 -3
  14. package/dist/effect/Logger.js.map +1 -1
  15. package/dist/effect/OtelTracer.d.ts +5 -0
  16. package/dist/effect/OtelTracer.d.ts.map +1 -0
  17. package/dist/effect/OtelTracer.js +8 -0
  18. package/dist/effect/OtelTracer.js.map +1 -0
  19. package/dist/effect/RpcClient.d.ts +32 -0
  20. package/dist/effect/RpcClient.d.ts.map +1 -0
  21. package/dist/effect/RpcClient.js +149 -0
  22. package/dist/effect/RpcClient.js.map +1 -0
  23. package/dist/effect/Schema/index.d.ts +2 -2
  24. package/dist/effect/Schema/index.d.ts.map +1 -1
  25. package/dist/effect/Schema/index.js +12 -2
  26. package/dist/effect/Schema/index.js.map +1 -1
  27. package/dist/effect/Stream.d.ts +73 -2
  28. package/dist/effect/Stream.d.ts.map +1 -1
  29. package/dist/effect/Stream.js +68 -1
  30. package/dist/effect/Stream.js.map +1 -1
  31. package/dist/effect/Stream.test.d.ts +2 -0
  32. package/dist/effect/Stream.test.d.ts.map +1 -0
  33. package/dist/effect/Stream.test.js +84 -0
  34. package/dist/effect/Stream.test.js.map +1 -0
  35. package/dist/effect/SubscriptionRef.d.ts +2 -2
  36. package/dist/effect/SubscriptionRef.d.ts.map +1 -1
  37. package/dist/effect/SubscriptionRef.js +6 -1
  38. package/dist/effect/SubscriptionRef.js.map +1 -1
  39. package/dist/effect/WebChannel/common.d.ts +1 -1
  40. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  41. package/dist/effect/WebSocket.js +1 -1
  42. package/dist/effect/WebSocket.js.map +1 -1
  43. package/dist/effect/index.d.ts +17 -11
  44. package/dist/effect/index.d.ts.map +1 -1
  45. package/dist/effect/index.js +20 -15
  46. package/dist/effect/index.js.map +1 -1
  47. package/dist/global.d.ts +1 -0
  48. package/dist/global.d.ts.map +1 -1
  49. package/dist/global.js.map +1 -1
  50. package/dist/misc.js +1 -1
  51. package/dist/misc.js.map +1 -1
  52. package/dist/mod.d.ts +2 -0
  53. package/dist/mod.d.ts.map +1 -1
  54. package/dist/mod.js +4 -0
  55. package/dist/mod.js.map +1 -1
  56. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -1
  57. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +66 -10
  58. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -1
  59. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js +177 -3
  60. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js.map +1 -1
  61. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +10 -1
  62. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts.map +1 -1
  63. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js +7 -1
  64. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js.map +1 -1
  65. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js +11 -1
  66. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js.map +1 -1
  67. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +16 -0
  68. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -1
  69. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +98 -2
  70. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
  71. package/dist/node/mod.d.ts +7 -1
  72. package/dist/node/mod.d.ts.map +1 -1
  73. package/dist/node/mod.js +10 -2
  74. package/dist/node/mod.js.map +1 -1
  75. package/package.json +42 -41
  76. package/src/NoopTracer.ts +1 -0
  77. package/src/effect/Effect.ts +31 -4
  78. package/src/effect/Error.ts +1 -1
  79. package/src/effect/Logger.ts +14 -4
  80. package/src/effect/OtelTracer.ts +11 -0
  81. package/src/effect/RpcClient.ts +212 -0
  82. package/src/effect/Schema/index.ts +17 -3
  83. package/src/effect/Stream.test.ts +127 -0
  84. package/src/effect/Stream.ts +111 -2
  85. package/src/effect/SubscriptionRef.ts +14 -2
  86. package/src/effect/WebChannel/common.ts +1 -1
  87. package/src/effect/WebSocket.ts +1 -1
  88. package/src/effect/index.ts +39 -14
  89. package/src/global.ts +1 -0
  90. package/src/misc.ts +1 -1
  91. package/src/mod.ts +9 -0
  92. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +71 -10
  93. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.ts +258 -3
  94. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/schema.ts +14 -1
  95. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.ts +14 -1
  96. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +111 -3
  97. package/src/node/mod.ts +12 -5
  98. package/dist/effect/Schema/msgpack.d.ts +0 -3
  99. package/dist/effect/Schema/msgpack.d.ts.map +0 -1
  100. package/dist/effect/Schema/msgpack.js +0 -7
  101. package/dist/effect/Schema/msgpack.js.map +0 -1
  102. package/src/effect/Schema/msgpack.ts +0 -8
@@ -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
+ })
@@ -15,7 +15,7 @@ export interface WebChannel<MsgListen, MsgSend, E = never> {
15
15
  closedDeferred: Deferred.Deferred<void>
16
16
  shutdown: Effect.Effect<void>
17
17
  schema: { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
18
- debugInfo?: Record<string, any>
18
+ debugInfo?: Record<string, any> | undefined
19
19
  }
20
20
 
21
21
  export const DebugPingMessage = Schema.TaggedStruct('WebChannel.DebugPing', {
@@ -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,22 @@
1
1
  import '../global.ts'
2
2
 
3
- export * as OtelTracer from '@effect/opentelemetry/Tracer'
3
+ export {
4
+ AiError,
5
+ LanguageModel,
6
+ LanguageModel as AiLanguageModel,
7
+ McpSchema,
8
+ McpServer,
9
+ Model,
10
+ Model as AiModel,
11
+ Prompt,
12
+ Tool,
13
+ Tool as AiTool,
14
+ Toolkit,
15
+ Toolkit as AiToolkit,
16
+ } from '@effect/ai'
17
+ // export { DevTools as EffectDevtools } from '@effect/experimental'
18
+ export { Sse } from '@effect/experimental'
19
+ export * as Otlp from '@effect/opentelemetry/Otlp'
4
20
  export {
5
21
  Command,
6
22
  CommandExecutor,
@@ -8,6 +24,11 @@ export {
8
24
  FetchHttpClient,
9
25
  FileSystem,
10
26
  Headers,
27
+ HttpApi,
28
+ HttpApiClient,
29
+ HttpApiEndpoint,
30
+ HttpApiGroup,
31
+ HttpApp,
11
32
  HttpClient,
12
33
  HttpClientError,
13
34
  HttpClientRequest,
@@ -18,6 +39,7 @@ export {
18
39
  HttpServerRequest,
19
40
  HttpServerResponse,
20
41
  KeyValueStore,
42
+ MsgPack,
21
43
  Socket,
22
44
  Terminal,
23
45
  Transferable,
@@ -29,7 +51,8 @@ export {
29
51
  export { BrowserWorker, BrowserWorkerRunner } from '@effect/platform-browser'
30
52
  export {
31
53
  Rpc,
32
- RpcClient,
54
+ // RpcClient, // TODO bring back "original" RpcClient from effect/rpc
55
+ RpcClientError,
33
56
  RpcGroup,
34
57
  RpcMessage,
35
58
  RpcMiddleware,
@@ -47,8 +70,8 @@ export {
47
70
  Cause,
48
71
  Channel,
49
72
  Chunk,
50
- // Logger,
51
73
  Config,
74
+ ConfigError,
52
75
  Console,
53
76
  Context,
54
77
  Data,
@@ -58,6 +81,7 @@ export {
58
81
  Equal,
59
82
  ExecutionStrategy,
60
83
  Exit,
84
+ FastCheck,
61
85
  Fiber,
62
86
  FiberHandle,
63
87
  FiberId,
@@ -98,6 +122,7 @@ export {
98
122
  Runtime,
99
123
  RuntimeFlags,
100
124
  Scope,
125
+ Sink,
101
126
  SortedMap,
102
127
  STM,
103
128
  SynchronizedRef,
@@ -107,26 +132,26 @@ export {
107
132
  Tracer,
108
133
  Types,
109
134
  } from 'effect'
110
- export { dual } from 'effect/Function'
135
+ export type { NonEmptyArray } from 'effect/Array'
136
+ export { constVoid, dual } from 'effect/Function'
137
+ export * as Graph from 'effect/Graph'
111
138
  export { TreeFormatter } from 'effect/ParseResult'
112
139
  export type { Serializable, SerializableWithResult } from 'effect/Schema'
113
-
114
140
  export * as SchemaAST from 'effect/SchemaAST'
115
141
  export * as BucketQueue from './BucketQueue.ts'
142
+ export * as Effect from './Effect.ts'
143
+ export * from './Error.ts'
116
144
  export * as Logger from './Logger.ts'
145
+ export * as OtelTracer from './OtelTracer.ts'
146
+ export * as RpcClient from './RpcClient.ts'
147
+ export * as Schedule from './Schedule.ts'
148
+ export * as Scheduler from './Scheduler.ts'
117
149
  export * as Schema from './Schema/index.ts'
150
+ export * as ServiceContext from './ServiceContext.ts'
118
151
  export * as Stream from './Stream.ts'
119
152
  export * as Subscribable from './Subscribable.ts'
120
153
  export * as SubscriptionRef from './SubscriptionRef.ts'
121
154
  export * as TaskTracing from './TaskTracing.ts'
122
155
  export * as WebChannel from './WebChannel/mod.ts'
123
- export * as WebSocket from './WebSocket.ts'
124
-
125
- // export { DevTools as EffectDevtools } from '@effect/experimental'
126
-
127
- export * as Effect from './Effect.ts'
128
- export * from './Error.ts'
129
- export * as Schedule from './Schedule.ts'
130
- export * as Scheduler from './Scheduler.ts'
131
- export * as ServiceContext from './ServiceContext.ts'
132
156
  export * as WebLock from './WebLock.ts'
157
+ export * as WebSocket from './WebSocket.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/misc.ts CHANGED
@@ -10,7 +10,7 @@ export const isDevEnv = () => {
10
10
  }
11
11
 
12
12
  // @ts-expect-error Only exists in Expo / RN
13
- if (typeof globalThis !== 'undefined' && globalThis.__DEV__) {
13
+ if (globalThis?.__DEV__) {
14
14
  return true
15
15
  }
16
16
 
package/src/mod.ts CHANGED
@@ -234,4 +234,13 @@ export const isPromise = (value: any): value is Promise<unknown> => typeof value
234
234
 
235
235
  export const isIterable = <T>(value: any): value is Iterable<T> => typeof value?.[Symbol.iterator] === 'function'
236
236
 
237
+ /** This utility "lies" as a means of compat with libs that don't explicitly type optionals as unioned with `undefined`. */
238
+ export const omitUndefineds = <T extends Record<keyof any, unknown>>(
239
+ rec: T,
240
+ ): {
241
+ [K in keyof T]: Exclude<T[K], undefined>
242
+ } => {
243
+ return rec as never
244
+ }
245
+
237
246
  export { objectToString as errorToString } from './misc.ts'
@@ -13,6 +13,53 @@ import * as Layer from 'effect/Layer'
13
13
  import * as Runtime from 'effect/Runtime'
14
14
  import * as Scope from 'effect/Scope'
15
15
 
16
+ // Parent death monitoring setup
17
+ let parentDeathDetectionEnabled = false
18
+ let parentDeathTimer: NodeJS.Timeout | null = null
19
+
20
+ const stopParentDeathMonitoring = () => {
21
+ parentDeathDetectionEnabled = false
22
+ if (parentDeathTimer) {
23
+ clearTimeout(parentDeathTimer)
24
+ parentDeathTimer = null
25
+ }
26
+ }
27
+
28
+ const setupParentDeathMonitoring = (parentPid: number) => {
29
+ if (parentDeathDetectionEnabled) return
30
+ parentDeathDetectionEnabled = true
31
+
32
+ let consecutiveFailures = 0
33
+ const maxFailures = 3 // Require 3 consecutive failures before self-terminating
34
+
35
+ // Check if parent is still alive every 2 seconds (more conservative)
36
+ const checkParentAlive = () => {
37
+ if (!parentDeathDetectionEnabled) return
38
+ try {
39
+ // Send signal 0 to check if process exists (doesn't actually send signal)
40
+ process.kill(parentPid, 0)
41
+ // If we reach here, parent is still alive, reset failure counter and check again later
42
+ consecutiveFailures = 0
43
+ parentDeathTimer = setTimeout(checkParentAlive, 2000)
44
+ } catch {
45
+ consecutiveFailures++
46
+ console.warn(`[Worker ${process.pid}] Parent check failed (${consecutiveFailures}/${maxFailures})`)
47
+
48
+ if (consecutiveFailures >= maxFailures) {
49
+ // Parent process has been gone for multiple checks, self-terminate
50
+ console.error(`[Worker ${process.pid}] Parent process ${parentPid} confirmed dead, self-terminating`)
51
+ process.exit(0)
52
+ } else {
53
+ // Try again sooner on failure
54
+ parentDeathTimer = setTimeout(checkParentAlive, 1000)
55
+ }
56
+ }
57
+ }
58
+
59
+ // Start monitoring after a longer initial delay to let things settle
60
+ parentDeathTimer = setTimeout(checkParentAlive, 5000)
61
+ }
62
+
16
63
  const platformRunnerImpl = Runner.PlatformRunner.of({
17
64
  [Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId,
18
65
  start<I, O>(closeLatch: typeof CloseLatch.Service) {
@@ -43,18 +90,32 @@ const platformRunnerImpl = Runner.PlatformRunner.of({
43
90
  Deferred.unsafeDone(closeLatch, Exit.die(exit.cause))
44
91
  }
45
92
  }
46
- port.on('message', (message: Runner.BackingRunner.Message<I>) => {
93
+ port.on('message', (message: Runner.BackingRunner.Message<I> | any) => {
47
94
  // console.log('message', message)
48
- if (message[0] === 0) {
49
- const result = handler(0, message[1])
50
- if (Effect.isEffect(result)) {
51
- const fiber = runFork(result)
52
- fiber.addObserver(onExit)
53
- FiberSet.unsafeAdd(fiberSet, fiber)
95
+
96
+ // Handle parent death detection setup messages
97
+ if (Array.isArray(message) && message[0] === 'setup-parent-death-detection' && message[1]?.parentPid) {
98
+ const parentPid = message[1].parentPid
99
+ // console.log(`[Worker ${process.pid}] Setting up parent death detection for parent ${parentPid}`)
100
+ setupParentDeathMonitoring(parentPid)
101
+ return
102
+ }
103
+
104
+ // Handle normal Effect worker messages
105
+ if (Array.isArray(message) && typeof message[0] === 'number') {
106
+ if (message[0] === 0) {
107
+ const result = handler(0, message[1])
108
+ if (Effect.isEffect(result)) {
109
+ const fiber = runFork(result)
110
+ fiber.addObserver(onExit)
111
+ FiberSet.unsafeAdd(fiberSet, fiber)
112
+ }
113
+ } else {
114
+ // Graceful shutdown requested by parent: stop monitoring and close port
115
+ stopParentDeathMonitoring()
116
+ Deferred.unsafeDone(closeLatch, Exit.void)
117
+ port.close()
54
118
  }
55
- } else {
56
- Deferred.unsafeDone(closeLatch, Exit.void)
57
- port.close()
58
119
  }
59
120
  })
60
121
  port.on('messageerror', (cause) => {