@livestore/utils 0.4.0 → 0.5.0-dev.0

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 (247) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/NoopTracer.d.ts +7 -8
  3. package/dist/NoopTracer.d.ts.map +1 -1
  4. package/dist/NoopTracer.js +3 -18
  5. package/dist/NoopTracer.js.map +1 -1
  6. package/dist/NoopTracer.test.d.ts +2 -0
  7. package/dist/NoopTracer.test.d.ts.map +1 -0
  8. package/dist/NoopTracer.test.js +17 -0
  9. package/dist/NoopTracer.test.js.map +1 -0
  10. package/dist/browser/Opfs/Opfs.d.ts +179 -29
  11. package/dist/browser/Opfs/Opfs.d.ts.map +1 -1
  12. package/dist/browser/Opfs/Opfs.js +132 -307
  13. package/dist/browser/Opfs/Opfs.js.map +1 -1
  14. package/dist/browser/Opfs/debug-utils.d.ts.map +1 -1
  15. package/dist/browser/Opfs/debug-utils.js +5 -4
  16. package/dist/browser/Opfs/debug-utils.js.map +1 -1
  17. package/dist/browser/Opfs/utils.d.ts +8 -8
  18. package/dist/browser/Opfs/utils.d.ts.map +1 -1
  19. package/dist/browser/Opfs/utils.js +35 -26
  20. package/dist/browser/Opfs/utils.js.map +1 -1
  21. package/dist/browser/QuotaExceededError.d.ts +10 -9
  22. package/dist/browser/QuotaExceededError.d.ts.map +1 -1
  23. package/dist/browser/WebChannelBrowser.d.ts.map +1 -1
  24. package/dist/browser/WebChannelBrowser.js +21 -10
  25. package/dist/browser/WebChannelBrowser.js.map +1 -1
  26. package/dist/browser/WebError.d.ts +54 -86
  27. package/dist/browser/WebError.d.ts.map +1 -1
  28. package/dist/browser/WebError.js +91 -84
  29. package/dist/browser/WebError.js.map +1 -1
  30. package/dist/browser/WebError.test.js +6 -6
  31. package/dist/browser/WebError.test.js.map +1 -1
  32. package/dist/browser/WebLock.d.ts.map +1 -1
  33. package/dist/browser/WebLock.js +13 -13
  34. package/dist/browser/WebLock.js.map +1 -1
  35. package/dist/effect/Debug.d.ts +4 -7
  36. package/dist/effect/Debug.d.ts.map +1 -1
  37. package/dist/effect/Debug.js +32 -36
  38. package/dist/effect/Debug.js.map +1 -1
  39. package/dist/effect/Effect.d.ts +31 -22
  40. package/dist/effect/Effect.d.ts.map +1 -1
  41. package/dist/effect/Effect.js +51 -47
  42. package/dist/effect/Effect.js.map +1 -1
  43. package/dist/effect/Effect.test.d.ts +2 -0
  44. package/dist/effect/Effect.test.d.ts.map +1 -0
  45. package/dist/effect/Effect.test.js +72 -0
  46. package/dist/effect/Effect.test.js.map +1 -0
  47. package/dist/effect/Error.d.ts +4 -6
  48. package/dist/effect/Error.d.ts.map +1 -1
  49. package/dist/effect/Logger.d.ts +0 -6
  50. package/dist/effect/Logger.d.ts.map +1 -1
  51. package/dist/effect/Logger.js +0 -32
  52. package/dist/effect/Logger.js.map +1 -1
  53. package/dist/effect/OtelTracer.d.ts +1 -1
  54. package/dist/effect/OtelTracer.d.ts.map +1 -1
  55. package/dist/effect/OtelTracer.js +2 -3
  56. package/dist/effect/OtelTracer.js.map +1 -1
  57. package/dist/effect/RpcClient.d.ts +7 -7
  58. package/dist/effect/RpcClient.d.ts.map +1 -1
  59. package/dist/effect/RpcClient.js +72 -52
  60. package/dist/effect/RpcClient.js.map +1 -1
  61. package/dist/effect/Schedule.d.ts +1 -1
  62. package/dist/effect/Schedule.d.ts.map +1 -1
  63. package/dist/effect/Schedule.js +2 -2
  64. package/dist/effect/Schedule.js.map +1 -1
  65. package/dist/effect/Schema/debug-diff.d.ts +1 -1
  66. package/dist/effect/Schema/debug-diff.d.ts.map +1 -1
  67. package/dist/effect/Schema/debug-diff.js +5 -7
  68. package/dist/effect/Schema/debug-diff.js.map +1 -1
  69. package/dist/effect/Schema/debug-diff.test.js +7 -4
  70. package/dist/effect/Schema/debug-diff.test.js.map +1 -1
  71. package/dist/effect/Schema/index.d.ts +28 -20
  72. package/dist/effect/Schema/index.d.ts.map +1 -1
  73. package/dist/effect/Schema/index.js +55 -28
  74. package/dist/effect/Schema/index.js.map +1 -1
  75. package/dist/effect/Stream.d.ts +13 -5
  76. package/dist/effect/Stream.d.ts.map +1 -1
  77. package/dist/effect/Stream.js +19 -12
  78. package/dist/effect/Stream.js.map +1 -1
  79. package/dist/effect/Stream.test.js +9 -9
  80. package/dist/effect/Stream.test.js.map +1 -1
  81. package/dist/effect/Subscribable.d.ts +9 -3
  82. package/dist/effect/Subscribable.d.ts.map +1 -1
  83. package/dist/effect/Subscribable.js +20 -35
  84. package/dist/effect/Subscribable.js.map +1 -1
  85. package/dist/effect/SubscriptionRef.d.ts +5 -6
  86. package/dist/effect/SubscriptionRef.d.ts.map +1 -1
  87. package/dist/effect/SubscriptionRef.js +2 -3
  88. package/dist/effect/SubscriptionRef.js.map +1 -1
  89. package/dist/effect/TaskTracing.d.ts +1 -1
  90. package/dist/effect/TaskTracing.d.ts.map +1 -1
  91. package/dist/effect/TaskTracing.js +12 -16
  92. package/dist/effect/TaskTracing.js.map +1 -1
  93. package/dist/effect/WebChannel/WebChannel.d.ts +7 -7
  94. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -1
  95. package/dist/effect/WebChannel/WebChannel.js +64 -43
  96. package/dist/effect/WebChannel/WebChannel.js.map +1 -1
  97. package/dist/effect/WebChannel/WebChannel.test.js +15 -15
  98. package/dist/effect/WebChannel/WebChannel.test.js.map +1 -1
  99. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts.map +1 -1
  100. package/dist/effect/WebChannel/broadcastChannelWithAck.js +16 -9
  101. package/dist/effect/WebChannel/broadcastChannelWithAck.js.map +1 -1
  102. package/dist/effect/WebChannel/common.d.ts +16 -17
  103. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  104. package/dist/effect/WebChannel/common.js +6 -6
  105. package/dist/effect/WebChannel/common.js.map +1 -1
  106. package/dist/effect/WebSocket.d.ts +4 -6
  107. package/dist/effect/WebSocket.d.ts.map +1 -1
  108. package/dist/effect/WebSocket.js +4 -4
  109. package/dist/effect/WebSocket.js.map +1 -1
  110. package/dist/effect/WebSocket.test.js +2 -2
  111. package/dist/effect/WebSocket.test.js.map +1 -1
  112. package/dist/effect/mod.browser-build.test.d.ts +2 -0
  113. package/dist/effect/mod.browser-build.test.d.ts.map +1 -0
  114. package/dist/effect/mod.browser-build.test.js +18 -0
  115. package/dist/effect/mod.browser-build.test.js.map +1 -0
  116. package/dist/effect/mod.d.ts +13 -15
  117. package/dist/effect/mod.d.ts.map +1 -1
  118. package/dist/effect/mod.js +13 -18
  119. package/dist/effect/mod.js.map +1 -1
  120. package/dist/effect/spanEvent.d.ts.map +1 -1
  121. package/dist/effect/spanEvent.js +2 -2
  122. package/dist/effect/spanEvent.js.map +1 -1
  123. package/dist/effect/spanEvent.test.js +10 -13
  124. package/dist/effect/spanEvent.test.js.map +1 -1
  125. package/dist/fast-deep-equal.d.ts.map +1 -1
  126. package/dist/fast-deep-equal.js +1 -2
  127. package/dist/fast-deep-equal.js.map +1 -1
  128. package/dist/misc.js +2 -2
  129. package/dist/misc.js.map +1 -1
  130. package/dist/mod.d.ts +0 -35
  131. package/dist/mod.d.ts.map +1 -1
  132. package/dist/mod.js +0 -8
  133. package/dist/mod.js.map +1 -1
  134. package/dist/node/mod.d.ts +17 -11
  135. package/dist/node/mod.d.ts.map +1 -1
  136. package/dist/node/mod.js +23 -16
  137. package/dist/node/mod.js.map +1 -1
  138. package/dist/object/index.d.ts +0 -9
  139. package/dist/object/index.d.ts.map +1 -1
  140. package/dist/object/index.js +0 -9
  141. package/dist/object/index.js.map +1 -1
  142. package/dist/string.d.ts +0 -2
  143. package/dist/string.d.ts.map +1 -1
  144. package/dist/string.js +0 -2
  145. package/dist/string.js.map +1 -1
  146. package/package.json +18 -36
  147. package/src/NoopTracer.test.ts +20 -0
  148. package/src/NoopTracer.ts +18 -41
  149. package/src/browser/Opfs/Opfs.ts +304 -277
  150. package/src/browser/Opfs/debug-utils.ts +7 -6
  151. package/src/browser/Opfs/utils.ts +36 -26
  152. package/src/browser/QuotaExceededError.ts +13 -10
  153. package/src/browser/WebChannelBrowser.ts +25 -11
  154. package/src/browser/WebError.test.ts +6 -6
  155. package/src/browser/WebError.ts +107 -133
  156. package/src/browser/WebLock.ts +13 -14
  157. package/src/effect/Debug.ts +66 -59
  158. package/src/effect/Effect.test.ts +117 -0
  159. package/src/effect/Effect.ts +84 -71
  160. package/src/effect/Logger.ts +0 -48
  161. package/src/effect/OtelTracer.ts +2 -3
  162. package/src/effect/RpcClient.ts +80 -61
  163. package/src/effect/Schedule.ts +4 -5
  164. package/src/effect/Schema/debug-diff.test.ts +6 -6
  165. package/src/effect/Schema/debug-diff.ts +6 -8
  166. package/src/effect/Schema/index.ts +122 -61
  167. package/src/effect/Stream.test.ts +9 -9
  168. package/src/effect/Stream.ts +23 -16
  169. package/src/effect/Subscribable.ts +31 -42
  170. package/src/effect/SubscriptionRef.ts +8 -12
  171. package/src/effect/TaskTracing.ts +13 -16
  172. package/src/effect/WebChannel/WebChannel.test.ts +23 -23
  173. package/src/effect/WebChannel/WebChannel.ts +87 -55
  174. package/src/effect/WebChannel/broadcastChannelWithAck.ts +21 -11
  175. package/src/effect/WebChannel/common.ts +14 -15
  176. package/src/effect/WebSocket.test.ts +2 -2
  177. package/src/effect/WebSocket.ts +9 -6
  178. package/src/effect/mod.browser-build.test.ts +19 -0
  179. package/src/effect/mod.ts +35 -69
  180. package/src/effect/spanEvent.test.ts +10 -13
  181. package/src/effect/spanEvent.ts +5 -3
  182. package/src/fast-deep-equal.ts +1 -2
  183. package/src/misc.ts +2 -2
  184. package/src/mod.ts +0 -36
  185. package/src/node/mod.ts +26 -20
  186. package/src/object/index.ts +0 -24
  187. package/src/string.ts +0 -3
  188. package/dist/effect/BucketQueue.d.ts +0 -20
  189. package/dist/effect/BucketQueue.d.ts.map +0 -1
  190. package/dist/effect/BucketQueue.js +0 -34
  191. package/dist/effect/BucketQueue.js.map +0 -1
  192. package/dist/effect/Scheduler.d.ts +0 -4
  193. package/dist/effect/Scheduler.d.ts.map +0 -1
  194. package/dist/effect/Scheduler.js +0 -9
  195. package/dist/effect/Scheduler.js.map +0 -1
  196. package/dist/effect/ServiceContext.d.ts +0 -37
  197. package/dist/effect/ServiceContext.d.ts.map +0 -1
  198. package/dist/effect/ServiceContext.js +0 -55
  199. package/dist/effect/ServiceContext.js.map +0 -1
  200. package/dist/guards.d.ts +0 -20
  201. package/dist/guards.d.ts.map +0 -1
  202. package/dist/guards.js +0 -20
  203. package/dist/guards.js.map +0 -1
  204. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts +0 -4
  205. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +0 -1
  206. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +0 -128
  207. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +0 -1
  208. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.d.ts +0 -10
  209. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.d.ts.map +0 -1
  210. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js +0 -217
  211. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js.map +0 -1
  212. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +0 -84
  213. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts.map +0 -1
  214. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js +0 -68
  215. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js.map +0 -1
  216. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.d.ts +0 -2
  217. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.d.ts.map +0 -1
  218. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js +0 -52
  219. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js.map +0 -1
  220. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +0 -23
  221. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +0 -1
  222. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +0 -138
  223. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +0 -1
  224. package/dist/object/omit.d.ts +0 -3
  225. package/dist/object/omit.d.ts.map +0 -1
  226. package/dist/object/omit.js +0 -14
  227. package/dist/object/omit.js.map +0 -1
  228. package/dist/object/pick.d.ts +0 -14
  229. package/dist/object/pick.d.ts.map +0 -1
  230. package/dist/object/pick.js +0 -17
  231. package/dist/object/pick.js.map +0 -1
  232. package/dist/time.d.ts +0 -12
  233. package/dist/time.d.ts.map +0 -1
  234. package/dist/time.js +0 -22
  235. package/dist/time.js.map +0 -1
  236. package/src/effect/BucketQueue.ts +0 -49
  237. package/src/effect/Scheduler.ts +0 -13
  238. package/src/effect/ServiceContext.ts +0 -108
  239. package/src/guards.ts +0 -23
  240. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +0 -148
  241. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.ts +0 -311
  242. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/schema.ts +0 -78
  243. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.ts +0 -65
  244. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +0 -169
  245. package/src/object/omit.ts +0 -17
  246. package/src/object/pick.ts +0 -27
  247. package/src/time.ts +0 -25
@@ -1,148 +0,0 @@
1
- import process from 'node:process'
2
-
3
- import { WorkerError } from '@effect/platform/WorkerError'
4
- import type { CloseLatch } from '@effect/platform/WorkerRunner'
5
- import * as Runner from '@effect/platform/WorkerRunner'
6
- import * as Cause from 'effect/Cause'
7
- import * as Context from 'effect/Context'
8
- import * as Deferred from 'effect/Deferred'
9
- import * as Effect from 'effect/Effect'
10
- import * as Exit from 'effect/Exit'
11
- import * as FiberSet from 'effect/FiberSet'
12
- import * as Layer from 'effect/Layer'
13
- import * as Runtime from 'effect/Runtime'
14
- import * as Scope from 'effect/Scope'
15
-
16
- // Parent death monitoring setup
17
- let parentDeathDetectionEnabled = false
18
- let parentDeathTimer: NodeJS.Timeout | null = null
19
-
20
- type SetupParentDeathDetectionMessage = ['setup-parent-death-detection', { parentPid: number }]
21
- type RunnerMessage<I> = Runner.BackingRunner.Message<I> | SetupParentDeathDetectionMessage
22
-
23
- const isSetupParentDeathDetectionMessage = (
24
- message: unknown,
25
- ): message is SetupParentDeathDetectionMessage =>
26
- Array.isArray(message) &&
27
- message[0] === 'setup-parent-death-detection' &&
28
- typeof message[1] === 'object' &&
29
- message[1] !== null &&
30
- 'parentPid' in message[1] &&
31
- typeof (message[1] as { parentPid: unknown }).parentPid === 'number'
32
-
33
- const stopParentDeathMonitoring = () => {
34
- parentDeathDetectionEnabled = false
35
- if (parentDeathTimer !== null) {
36
- clearTimeout(parentDeathTimer)
37
- parentDeathTimer = null
38
- }
39
- }
40
-
41
- const setupParentDeathMonitoring = (parentPid: number) => {
42
- if (parentDeathDetectionEnabled === true) return
43
- parentDeathDetectionEnabled = true
44
-
45
- let consecutiveFailures = 0
46
- const maxFailures = 3 // Require 3 consecutive failures before self-terminating
47
-
48
- // Check if parent is still alive every 2 seconds (more conservative)
49
- const checkParentAlive = () => {
50
- if (parentDeathDetectionEnabled === false) return
51
- try {
52
- // Send signal 0 to check if process exists (doesn't actually send signal)
53
- process.kill(parentPid, 0)
54
- // If we reach here, parent is still alive, reset failure counter and check again later
55
- consecutiveFailures = 0
56
- parentDeathTimer = setTimeout(checkParentAlive, 2000)
57
- } catch {
58
- consecutiveFailures++
59
- console.warn(`[Worker ${process.pid}] Parent check failed (${consecutiveFailures}/${maxFailures})`)
60
-
61
- if (consecutiveFailures >= maxFailures) {
62
- // Parent process has been gone for multiple checks, self-terminate
63
- console.error(`[Worker ${process.pid}] Parent process ${parentPid} confirmed dead, self-terminating`)
64
- process.exit(0)
65
- } else {
66
- // Try again sooner on failure
67
- parentDeathTimer = setTimeout(checkParentAlive, 1000)
68
- }
69
- }
70
- }
71
-
72
- // Start monitoring after a longer initial delay to let things settle
73
- parentDeathTimer = setTimeout(checkParentAlive, 5000)
74
- }
75
-
76
- const platformRunnerImpl = Runner.PlatformRunner.of({
77
- [Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId,
78
- start<I, O>(closeLatch: typeof CloseLatch.Service) {
79
- return Effect.gen(function* () {
80
- if (process.send == null) {
81
- return yield* new WorkerError({ reason: 'spawn', cause: new Error('not in a child process') })
82
- }
83
- const port = {
84
- postMessage: (message: any) => process.send!(message),
85
- on: (event: string, handler: (message: any) => void) => process.on(event, handler),
86
- close: () => process.disconnect?.(),
87
- }
88
- const send = (_portId: number, message: O, _transfers?: ReadonlyArray<unknown>) =>
89
- Effect.sync(() => port.postMessage([1, message] /*, transfers as any*/))
90
-
91
- const run = Effect.fnUntraced(function* <A, E, R>(
92
- // biome-ignore lint/suspicious/noConfusingVoidType: need to support void
93
- handler: (portId: number, message: I) => Effect.Effect<A, E, R> | void,
94
- ) {
95
- const runtime = (yield* Effect.interruptible(Effect.runtime<R | Scope.Scope>())).pipe(
96
- Runtime.updateContext(Context.omit(Scope.Scope)),
97
- ) as Runtime.Runtime<R>
98
- const fiberSet = yield* FiberSet.make<any, WorkerError | E>()
99
- const runFork = Runtime.runFork(runtime)
100
- const onExit = (exit: Exit.Exit<any, E>) => {
101
- if (exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause) === false) {
102
- // Deferred.unsafeDone(closeLatch, Exit.die(Cause.squash(exit.cause)))
103
- Deferred.unsafeDone(closeLatch, Exit.die(exit.cause))
104
- }
105
- }
106
- port.on('message', (message: RunnerMessage<I>) => {
107
- // console.log('message', message)
108
-
109
- // Handle parent death detection setup messages
110
- if (isSetupParentDeathDetectionMessage(message) === true) {
111
- const parentPid = message[1].parentPid
112
- // console.log(`[Worker ${process.pid}] Setting up parent death detection for parent ${parentPid}`)
113
- setupParentDeathMonitoring(parentPid)
114
- return
115
- }
116
-
117
- // Handle normal Effect worker messages
118
- if (Array.isArray(message) === true && typeof message[0] === 'number') {
119
- if (message[0] === 0) {
120
- const result = handler(0, message[1])
121
- if (Effect.isEffect(result) === true) {
122
- const fiber = runFork(result)
123
- fiber.addObserver(onExit)
124
- FiberSet.unsafeAdd(fiberSet, fiber)
125
- }
126
- } else {
127
- // Graceful shutdown requested by parent: stop monitoring and close port
128
- stopParentDeathMonitoring()
129
- Deferred.unsafeDone(closeLatch, Exit.void)
130
- port.close()
131
- }
132
- }
133
- })
134
- port.on('messageerror', (cause) => {
135
- Deferred.unsafeDone(closeLatch, new WorkerError({ reason: 'decode', cause }))
136
- })
137
- port.on('error', (cause) => {
138
- Deferred.unsafeDone(closeLatch, new WorkerError({ reason: 'unknown', cause }))
139
- })
140
- port.postMessage([0])
141
- })
142
-
143
- return { run, send }
144
- })
145
- },
146
- })
147
-
148
- export const layer = Layer.succeed(Runner.PlatformRunner, platformRunnerImpl)
@@ -1,311 +0,0 @@
1
- import * as ChildProcess from 'node:child_process'
2
-
3
- import * as EffectWorker from '@effect/platform/Worker'
4
- import { assert, describe, it } from '@effect/vitest'
5
- import { Chunk, Deferred, Effect, Exit, Fiber, Schema, Scope, Stream } from 'effect'
6
-
7
- export class TestError extends Schema.TaggedError<TestError>()('TestError', {
8
- message: Schema.String,
9
- }) {}
10
-
11
- import * as ChildProcessWorker from '../ChildProcessWorker.ts'
12
- import type { WorkerMessage } from './schema.ts'
13
- import { GetPersonById, GetUserById, InitialMessage, Person, StartStubbornWorker, User } from './schema.ts'
14
-
15
- const WorkerLive = ChildProcessWorker.layer(() =>
16
- ChildProcess.fork(
17
- new URL('../../../../dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js', import.meta.url),
18
- ),
19
- )
20
-
21
- // const WorkerLive = NodeWorker.layer(
22
- // () =>
23
- // new WorkerThreads.Worker(
24
- // new URL('../../../../dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js', import.meta.url),
25
- // ),
26
- // )
27
-
28
- describe('ChildProcessRunner', { timeout: 10_000 }, () => {
29
- it('Serialized', () =>
30
- Effect.gen(function* () {
31
- const pool = yield* EffectWorker.makePoolSerialized({ size: 1 })
32
- const people = yield* pool.execute(new GetPersonById({ id: 123 })).pipe(Stream.runCollect)
33
- assert.deepStrictEqual(Chunk.toReadonlyArray(people), [
34
- new Person({ id: 123, name: 'test', data: new Uint8Array([1, 2, 3]) }),
35
- new Person({ id: 123, name: 'ing', data: new Uint8Array([4, 5, 6]) }),
36
- ])
37
- }).pipe(Effect.scoped, Effect.provide(WorkerLive), Effect.runPromise))
38
-
39
- it('Serialized with initialMessage', () =>
40
- Effect.gen(function* () {
41
- const pool = yield* EffectWorker.makePoolSerialized<WorkerMessage>({
42
- size: 1,
43
- initialMessage: () => new InitialMessage({ name: 'custom', data: new Uint8Array([1, 2, 3]) }),
44
- })
45
-
46
- let user = yield* pool.executeEffect(new GetUserById({ id: 123 }))
47
- user = yield* pool.executeEffect(new GetUserById({ id: 123 }))
48
- assert.deepStrictEqual(user, new User({ id: 123, name: 'custom' }))
49
- const people = yield* pool.execute(new GetPersonById({ id: 123 })).pipe(Stream.runCollect)
50
- assert.deepStrictEqual(Chunk.toReadonlyArray(people), [
51
- new Person({ id: 123, name: 'test', data: new Uint8Array([1, 2, 3]) }),
52
- new Person({ id: 123, name: 'ing', data: new Uint8Array([4, 5, 6]) }),
53
- ])
54
- }).pipe(Effect.scoped, Effect.provide(WorkerLive), Effect.runPromise))
55
-
56
- describe('Process Cleanup', { timeout: 15_000 }, () => {
57
- const isProcessRunning = (pid: number) => {
58
- try {
59
- process.kill(pid, 0) // Signal 0 checks if process exists
60
- return true
61
- } catch {
62
- return false
63
- }
64
- }
65
-
66
- it('should clean up child processes when Effect is interrupted', () =>
67
- Effect.gen(function* () {
68
- const workerPidDeferred = yield* Deferred.make<number>()
69
-
70
- const testEffect = Effect.gen(function* () {
71
- const pool = yield* EffectWorker.makePoolSerialized<WorkerMessage>({
72
- size: 1,
73
- initialMessage: () => new InitialMessage({ name: 'test', data: new Uint8Array([1, 2, 3]) }),
74
- })
75
- const result = yield* pool.executeEffect(new StartStubbornWorker({ blockDuration: 30_000 }))
76
- yield* Deferred.succeed(workerPidDeferred, result.pid)
77
-
78
- // Verify the worker process is running
79
- assert.strictEqual(isProcessRunning(result.pid), true, 'Worker process should be running')
80
-
81
- // Start a long-running operation that we'll interrupt
82
- yield* Effect.sleep('60 seconds')
83
- }).pipe(Effect.scoped, Effect.provide(WorkerLive))
84
-
85
- // Run the test effect but interrupt it after 2 seconds
86
- const fiber = yield* Effect.forkScoped(testEffect)
87
-
88
- const workerPid = yield* Deferred.await(workerPidDeferred).pipe(
89
- Effect.raceFirst(
90
- Fiber.join(fiber).pipe(
91
- Effect.flatMap(() => new TestError({ message: 'testEffect completed before reporting worker PID' })),
92
- ),
93
- ),
94
- Effect.timeout(10_000),
95
- )
96
-
97
- yield* Effect.sleep('2 seconds')
98
- yield* Fiber.interrupt(fiber)
99
-
100
- // Wait a moment for cleanup to complete
101
- yield* Effect.sleep('1 second')
102
-
103
- assert.strictEqual(
104
- isProcessRunning(workerPid),
105
- false,
106
- `Worker process ${workerPid} should be terminated after Effect interruption`,
107
- )
108
- }).pipe(Effect.scoped, Effect.runPromise))
109
-
110
- it('should clean up child processes when scope is closed abruptly', () =>
111
- Effect.gen(function* () {
112
- let workerPid: number | undefined
113
-
114
- // Create a scope that we can close manually
115
- const scope = yield* Scope.make()
116
-
117
- try {
118
- const pool = yield* EffectWorker.makePoolSerialized<WorkerMessage>({
119
- size: 1,
120
- initialMessage: () => new InitialMessage({ name: 'test', data: new Uint8Array([1, 2, 3]) }),
121
- }).pipe(Scope.extend(scope), Effect.provide(WorkerLive))
122
-
123
- const result = yield* pool.executeEffect(new StartStubbornWorker({ blockDuration: 30_000 }))
124
- workerPid = result.pid
125
-
126
- // Verify the worker is running
127
- assert.strictEqual(isProcessRunning(workerPid), true, 'Worker process should be running')
128
- } finally {
129
- // Abruptly close the scope (simulating test abortion)
130
- yield* Scope.close(scope, Exit.void)
131
- }
132
-
133
- // Wait for cleanup
134
- yield* Effect.sleep('1 second')
135
-
136
- // This should pass but will initially fail due to zombie process issue
137
- if (workerPid !== undefined) {
138
- assert.strictEqual(
139
- isProcessRunning(workerPid),
140
- false,
141
- `Worker process ${workerPid} should be terminated after scope closure`,
142
- )
143
- } else {
144
- assert.fail('Worker PID was not captured')
145
- }
146
- }).pipe(Effect.runPromise))
147
-
148
- it('should clean up child processes when parent receives SIGINT', () =>
149
- Effect.gen(function* () {
150
- let workerPid: number | undefined
151
-
152
- const pool = yield* EffectWorker.makePoolSerialized<WorkerMessage>({
153
- size: 1,
154
- initialMessage: () => new InitialMessage({ name: 'test', data: new Uint8Array([1, 2, 3]) }),
155
- })
156
-
157
- const result = yield* pool.executeEffect(new StartStubbornWorker({ blockDuration: 60_000 }))
158
- workerPid = result.pid
159
-
160
- // Verify the worker is running
161
- assert.strictEqual(isProcessRunning(workerPid), true, 'Worker process should be running')
162
-
163
- // Simulate SIGINT being sent to current process (like Ctrl+C in vitest)
164
- // This should trigger cleanup of child processes
165
- yield* Effect.async<void>((resume) => {
166
- // Store current listeners before we manipulate them
167
- const currentSIGINTListeners = process.listeners('SIGINT').slice()
168
-
169
- // Set up our test handler
170
- const testHandler = () => {
171
- // Emit SIGINT to all current listeners to trigger cleanup
172
- currentSIGINTListeners.forEach((listener) => {
173
- try {
174
- ;(listener as Function)()
175
- } catch {
176
- // Ignore errors
177
- }
178
- })
179
- resume(Effect.void)
180
- }
181
-
182
- // Remove all current SIGINT listeners and add our test handler
183
- process.removeAllListeners('SIGINT')
184
- process.once('SIGINT', testHandler)
185
-
186
- // Send SIGINT after a short delay
187
- setTimeout(() => {
188
- process.kill(process.pid, 'SIGINT')
189
- }, 1000)
190
- })
191
-
192
- // Wait for cleanup to complete
193
- yield* Effect.sleep('2 seconds')
194
-
195
- // This test should initially fail - child process will still be running
196
- if (workerPid !== undefined) {
197
- assert.strictEqual(
198
- isProcessRunning(workerPid),
199
- false,
200
- `Worker process ${workerPid} should be terminated after SIGINT`,
201
- )
202
- } else {
203
- assert.fail('Worker PID was not captured')
204
- }
205
- }).pipe(Effect.scoped, Effect.provide(WorkerLive), Effect.runPromise))
206
-
207
- it('should clean up multiple concurrent child processes', () =>
208
- Effect.gen(function* () {
209
- let workerPids: number[] = []
210
-
211
- const testEffect = Effect.gen(function* () {
212
- const pool = yield* EffectWorker.makePoolSerialized<WorkerMessage>({
213
- size: 3, // Multiple workers
214
- initialMessage: () => new InitialMessage({ name: 'test', data: new Uint8Array([1, 2, 3]) }),
215
- })
216
-
217
- // Start multiple stubborn workers
218
- const workers = yield* Effect.all(
219
- [
220
- pool.executeEffect(new StartStubbornWorker({ blockDuration: 30_000 })),
221
- pool.executeEffect(new StartStubbornWorker({ blockDuration: 30_000 })),
222
- pool.executeEffect(new StartStubbornWorker({ blockDuration: 30_000 })),
223
- ],
224
- { concurrency: 'unbounded' },
225
- )
226
-
227
- workerPids = workers.map((w) => w.pid)
228
-
229
- // Verify all workers are running
230
- for (const pid of workerPids) {
231
- assert.strictEqual(isProcessRunning(pid), true, `Worker process ${pid} should be running`)
232
- }
233
-
234
- yield* Effect.sleep('30 seconds') // Keep running until interrupted
235
- }).pipe(Effect.scoped, Effect.provide(WorkerLive))
236
-
237
- // Run with timeout to force termination
238
- const fiber = yield* Effect.fork(testEffect)
239
- yield* Effect.sleep('2 seconds')
240
- yield* Fiber.interrupt(fiber)
241
-
242
- // Wait for cleanup
243
- yield* Effect.sleep('2 seconds')
244
-
245
- // All worker processes should be cleaned up
246
- for (const pid of workerPids) {
247
- assert.strictEqual(
248
- isProcessRunning(pid),
249
- false,
250
- `Worker process ${pid} should be terminated after pool cleanup`,
251
- )
252
- }
253
- }).pipe(Effect.runPromise))
254
-
255
- it('should handle direct ChildProcess.fork cleanup (node-sync pattern)', () =>
256
- Effect.gen(function* () {
257
- let childPid: number | undefined
258
-
259
- // This mimics the exact pattern used in node-sync tests
260
- const nodeChildProcess = ChildProcess.fork(
261
- new URL(
262
- '../../../../dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js',
263
- import.meta.url,
264
- ),
265
- ['test-client'],
266
- )
267
-
268
- childPid = nodeChildProcess.pid
269
-
270
- const testEffect = Effect.gen(function* () {
271
- const worker = yield* EffectWorker.makePoolSerialized<WorkerMessage>({
272
- size: 1,
273
- concurrency: 100,
274
- initialMessage: () => new InitialMessage({ name: 'test', data: new Uint8Array([1, 2, 3]) }),
275
- }).pipe(Effect.provide(ChildProcessWorker.layer(() => nodeChildProcess)))
276
-
277
- // Start stubborn worker
278
- yield* worker.executeEffect(new StartStubbornWorker({ blockDuration: 60_000 }))
279
-
280
- // Verify process is running
281
- if (childPid !== undefined) {
282
- assert.strictEqual(isProcessRunning(childPid), true, 'Child process should be running')
283
- }
284
-
285
- // Keep running until interrupted
286
- yield* Effect.sleep('30 seconds')
287
- }).pipe(Effect.scoped)
288
-
289
- // Simulate the exact abortion pattern from node-sync
290
- const fiber = yield* Effect.fork(testEffect)
291
- yield* Effect.sleep('2 seconds')
292
-
293
- // Force kill the fiber without proper cleanup (simulates Ctrl+C)
294
- yield* Fiber.interrupt(fiber)
295
-
296
- // Wait for cleanup
297
- yield* Effect.sleep('3 seconds')
298
-
299
- // This test should initially fail - demonstrating the zombie process issue
300
- if (childPid !== undefined) {
301
- assert.strictEqual(
302
- isProcessRunning(childPid),
303
- false,
304
- `Child process ${childPid} should be terminated after forced interruption`,
305
- )
306
- } else {
307
- assert.fail('Child PID was not captured')
308
- }
309
- }).pipe(Effect.runPromise))
310
- })
311
- })
@@ -1,78 +0,0 @@
1
- // import * as Transferable from '@effect/platform/Transferable'
2
- import * as Schema from 'effect/Schema'
3
-
4
- export class User extends Schema.Class<User>('User')({
5
- id: Schema.Number,
6
- name: Schema.String,
7
- }) {}
8
-
9
- export class GetUserById extends Schema.TaggedRequest<GetUserById>()('GetUserById', {
10
- failure: Schema.Never,
11
- success: User,
12
- payload: {
13
- id: Schema.Number,
14
- },
15
- }) {}
16
-
17
- export class Person extends Schema.Class<Person>('Person')({
18
- id: Schema.Number,
19
- name: Schema.String,
20
- // data: Transferable.Uint8Array,
21
- data: Schema.Uint8Array,
22
- }) {}
23
-
24
- export class GetPersonById extends Schema.TaggedRequest<GetPersonById>()('GetPersonById', {
25
- failure: Schema.Never,
26
- success: Person,
27
- payload: {
28
- id: Schema.Number,
29
- },
30
- }) {}
31
-
32
- export class RunnerInterrupt extends Schema.TaggedRequest<RunnerInterrupt>()('RunnerInterrupt', {
33
- failure: Schema.Never,
34
- success: Schema.Void,
35
- payload: {},
36
- }) {}
37
-
38
- export class StartStubbornWorker extends Schema.TaggedRequest<StartStubbornWorker>()('StartStubbornWorker', {
39
- failure: Schema.Never,
40
- success: Schema.Struct({ pid: Schema.Number }),
41
- payload: { blockDuration: Schema.Number },
42
- }) {}
43
-
44
- export class InitialMessage extends Schema.TaggedRequest<InitialMessage>()('InitialMessage', {
45
- failure: Schema.Never,
46
- success: Schema.Void,
47
- payload: {
48
- name: Schema.String,
49
- data: Schema.Uint8Array,
50
- // data: Transferable.Uint8Array,
51
- },
52
- }) {}
53
-
54
- export class GetSpan extends Schema.TaggedRequest<GetSpan>()('GetSpan', {
55
- failure: Schema.Never,
56
- success: Schema.Struct({
57
- name: Schema.String,
58
- traceId: Schema.String,
59
- spanId: Schema.String,
60
- parent: Schema.Option(
61
- Schema.Struct({
62
- traceId: Schema.String,
63
- spanId: Schema.String,
64
- }),
65
- ),
66
- }),
67
- payload: {},
68
- }) {}
69
-
70
- export const WorkerMessage = Schema.Union(
71
- GetUserById,
72
- GetPersonById,
73
- InitialMessage,
74
- GetSpan,
75
- RunnerInterrupt,
76
- StartStubbornWorker,
77
- )
78
- export type WorkerMessage = Schema.Schema.Type<typeof WorkerMessage>
@@ -1,65 +0,0 @@
1
- import * as Runner from '@effect/platform/WorkerRunner'
2
- import { Context, Effect, Layer, Option, Stream } from 'effect'
3
-
4
- // import { NodeRuntime, NodeWorkerRunner } from '@effect/platform-node'
5
- import { PlatformNode } from '../../mod.ts'
6
- import * as ChildProcessRunner from '../ChildProcessRunner.ts'
7
- import { Person, User, WorkerMessage } from './schema.ts'
8
-
9
- interface Name {
10
- readonly _: unique symbol
11
- }
12
- const Name = Context.GenericTag<Name, string>('Name')
13
-
14
- const WorkerLive = Runner.layerSerialized(WorkerMessage, {
15
- GetPersonById: (req) => {
16
- return Stream.make(
17
- new Person({ id: req.id, name: 'test', data: new Uint8Array([1, 2, 3]) }),
18
- new Person({ id: req.id, name: 'ing', data: new Uint8Array([4, 5, 6]) }),
19
- )
20
- },
21
- GetUserById: (req) => Effect.map(Name, (name) => new User({ id: req.id, name })),
22
- // InitialMessage: (req) => Layer.succeed(Name, req.name),
23
- InitialMessage: (req) =>
24
- Effect.gen(function* () {
25
- // yield* Effect.addFinalizer(() => Effect.log('closing worker scope'))
26
- return Layer.succeed(Name, req.name)
27
- }).pipe(Layer.unwrapScoped),
28
- // InitialMessage: (req) =>
29
- // Layer.scoped(
30
- // Name,
31
- // Effect.gen(function* () {
32
- // yield* Effect.addFinalizer(() => Effect.log('closing worker scope'))
33
- // return req.name
34
- // }),
35
- // ),
36
- GetSpan: Effect.fn('GetSpan')(function* (_) {
37
- const span = yield* Effect.currentSpan.pipe(Effect.orDie)
38
- return {
39
- traceId: span.traceId,
40
- spanId: span.spanId,
41
- name: span.name,
42
- parent: Option.map(span.parent, (span) => ({
43
- traceId: span.traceId,
44
- spanId: span.spanId,
45
- })),
46
- }
47
- }),
48
- RunnerInterrupt: () => Effect.interrupt,
49
- StartStubbornWorker: ({ blockDuration }) =>
50
- Effect.gen(function* () {
51
- // Start a blocking operation that won't respond to normal shutdown signals
52
- const pid = process.pid
53
- yield* Effect.fork(
54
- Effect.gen(function* () {
55
- // Block for the specified duration, ignoring shutdown attempts
56
- yield* Effect.sleep(`${blockDuration} millis`)
57
- yield* Effect.log('Stubborn worker finished blocking')
58
- }).pipe(Effect.uninterruptible),
59
- )
60
- return { pid }
61
- }),
62
- }).pipe(Layer.provide(ChildProcessRunner.layer))
63
- // }).pipe(Layer.provide(PlatformNode.NodeWorkerRunner.layer))
64
-
65
- PlatformNode.NodeRuntime.runMain(Runner.launch(WorkerLive))