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

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 +13 -3
  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 +40 -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 +16 -3
  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,9 @@ export {
47
70
  Cause,
48
71
  Channel,
49
72
  Chunk,
50
- // Logger,
51
73
  Config,
74
+ ConfigError,
75
+ ConfigProvider,
52
76
  Console,
53
77
  Context,
54
78
  Data,
@@ -58,6 +82,7 @@ export {
58
82
  Equal,
59
83
  ExecutionStrategy,
60
84
  Exit,
85
+ FastCheck,
61
86
  Fiber,
62
87
  FiberHandle,
63
88
  FiberId,
@@ -98,6 +123,7 @@ export {
98
123
  Runtime,
99
124
  RuntimeFlags,
100
125
  Scope,
126
+ Sink,
101
127
  SortedMap,
102
128
  STM,
103
129
  SynchronizedRef,
@@ -107,26 +133,26 @@ export {
107
133
  Tracer,
108
134
  Types,
109
135
  } from 'effect'
110
- export { dual } from 'effect/Function'
136
+ export type { NonEmptyArray } from 'effect/Array'
137
+ export { constVoid, dual } from 'effect/Function'
138
+ export * as Graph from 'effect/Graph'
111
139
  export { TreeFormatter } from 'effect/ParseResult'
112
140
  export type { Serializable, SerializableWithResult } from 'effect/Schema'
113
-
114
141
  export * as SchemaAST from 'effect/SchemaAST'
115
142
  export * as BucketQueue from './BucketQueue.ts'
143
+ export * as Effect from './Effect.ts'
144
+ export * from './Error.ts'
116
145
  export * as Logger from './Logger.ts'
146
+ export * as OtelTracer from './OtelTracer.ts'
147
+ export * as RpcClient from './RpcClient.ts'
148
+ export * as Schedule from './Schedule.ts'
149
+ export * as Scheduler from './Scheduler.ts'
117
150
  export * as Schema from './Schema/index.ts'
151
+ export * as ServiceContext from './ServiceContext.ts'
118
152
  export * as Stream from './Stream.ts'
119
153
  export * as Subscribable from './Subscribable.ts'
120
154
  export * as SubscriptionRef from './SubscriptionRef.ts'
121
155
  export * as TaskTracing from './TaskTracing.ts'
122
156
  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
157
  export * as WebLock from './WebLock.ts'
158
+ 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) => {