@sshkeda/task 0.0.0-stage → 1.0.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/package.json +41 -4
  4. package/src/index.ts +2 -0
  5. package/src/lib/errors/index.ts +15 -0
  6. package/src/lib/errors/internal/errors.ts +147 -0
  7. package/src/lib/kill-leftover-processes/index.ts +17 -0
  8. package/src/lib/kill-leftover-processes/internal/command-words.ts +16 -0
  9. package/src/lib/kill-leftover-processes/internal/find-process-ids.ts +36 -0
  10. package/src/lib/kill-leftover-processes/internal/kill-process-groups.ts +270 -0
  11. package/src/lib/kill-leftover-processes/internal/list-linux-processes-from-proc.ts +191 -0
  12. package/src/lib/kill-leftover-processes/internal/list-macos-processes-with-sysctl.ts +335 -0
  13. package/src/lib/kill-leftover-processes/internal/list-processes.ts +58 -0
  14. package/src/lib/kill-leftover-processes/internal/mark-run-processes.ts +367 -0
  15. package/src/lib/kill-leftover-processes/internal/process-cleanup-errors.ts +17 -0
  16. package/src/lib/kill-leftover-processes/internal/send-signal.ts +32 -0
  17. package/src/lib/kill-leftover-processes/internal/signal-run-processes.ts +48 -0
  18. package/src/lib/kill-leftover-processes/internal/started-process-groups.ts +107 -0
  19. package/src/lib/main-thread/index.ts +3 -0
  20. package/src/lib/main-thread/internal/listen-for-stop-requests.ts +102 -0
  21. package/src/lib/main-thread/internal/run-main-thread.ts +81 -0
  22. package/src/lib/main-thread/internal/run-root-task-fiber.ts +191 -0
  23. package/src/lib/main-thread/internal/stops-at-first-failure.ts +20 -0
  24. package/src/lib/main-thread/internal/trace-the-run.ts +107 -0
  25. package/src/lib/main-thread/lib/find-code-each-task-depends-on/index.ts +7 -0
  26. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/follow-imports-from-entry-files.ts +306 -0
  27. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/load-all-reporting-first-failure.ts +11 -0
  28. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/make-module-graph.ts +48 -0
  29. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/read-configured-loaders.ts +64 -0
  30. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/resolve-module-specifiers.ts +60 -0
  31. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/rewrite-worker-imports.ts +118 -0
  32. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/worker-import-errors.ts +19 -0
  33. package/src/lib/main-thread/lib/run-tasks-on-workers/index.ts +7 -0
  34. package/src/lib/main-thread/lib/run-tasks-on-workers/internal/handle-worker-events.ts +348 -0
  35. package/src/lib/main-thread/lib/run-tasks-on-workers/internal/make-task-runs.ts +207 -0
  36. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/index.ts +1 -0
  37. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/await-within-counted-limit.ts +29 -0
  38. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/execute-task.ts +272 -0
  39. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/make-task-setup-clock.ts +129 -0
  40. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/run-requested-tasks.ts +135 -0
  41. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/define-task-run-state.ts +288 -0
  42. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/make-stopwatch.ts +39 -0
  43. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/share-standard-input-with-root-task.ts +36 -0
  44. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/stop-task.ts +166 -0
  45. package/src/lib/main-thread/lib/scan-module-source/index.ts +1 -0
  46. package/src/lib/main-thread/lib/scan-module-source/internal/find-lexical-bindings.ts +154 -0
  47. package/src/lib/main-thread/lib/scan-module-source/internal/find-task-declarations.ts +205 -0
  48. package/src/lib/main-thread/lib/scan-module-source/internal/parse-module-text.ts +34 -0
  49. package/src/lib/main-thread/lib/scan-module-source/internal/read-literal-string.ts +15 -0
  50. package/src/lib/main-thread/lib/scan-module-source/internal/read-module-loads.ts +316 -0
  51. package/src/lib/main-thread/lib/scan-module-source/internal/scan-module-text.ts +70 -0
  52. package/src/lib/main-thread/lib/scan-module-source/internal/unwrap-type-assertions.ts +10 -0
  53. package/src/lib/main-thread/lib/scan-module-source/internal/visit-non-reference-identifiers.ts +21 -0
  54. package/src/lib/main-thread/shared/fail-run.ts +36 -0
  55. package/src/lib/main-thread/shared/find-failure-reason.ts +43 -0
  56. package/src/lib/main-thread/shared/find-package-directories.ts +34 -0
  57. package/src/lib/main-thread/shared/find-tasks-reachable-from-root-task-file.ts +246 -0
  58. package/src/lib/main-thread/shared/list-directory-paths-up-to-root.ts +13 -0
  59. package/src/lib/main-thread/shared/main-thread-errors.ts +373 -0
  60. package/src/lib/main-thread/shared/package-json-errors.ts +93 -0
  61. package/src/lib/main-thread/shared/provide-discovered-tasks.ts +26 -0
  62. package/src/lib/main-thread/shared/provide-process-groups.ts +63 -0
  63. package/src/lib/main-thread/shared/provide-run-root-task-config.ts +66 -0
  64. package/src/lib/main-thread/shared/provide-stop-state.ts +150 -0
  65. package/src/lib/main-thread/shared/provide-worker-pool.ts +607 -0
  66. package/src/lib/main-thread/shared/read-bare-package-name.ts +3 -0
  67. package/src/lib/main-thread/shared/read-package-json.ts +149 -0
  68. package/src/lib/main-thread/shared/report-task-failure.ts +31 -0
  69. package/src/lib/preload-rewritten-imports-into-each-worker/index.ts +5 -0
  70. package/src/lib/preload-rewritten-imports-into-each-worker/internal/load-rewritten-imports.ts +34 -0
  71. package/src/lib/supervise-the-run/index.ts +15 -0
  72. package/src/lib/supervise-the-run/internal/adopt-orphans-on-linux.ts +73 -0
  73. package/src/lib/supervise-the-run/internal/check-root-task-file.ts +8 -0
  74. package/src/lib/supervise-the-run/internal/report-ready.ts +12 -0
  75. package/src/lib/supervise-the-run/internal/share-process-groups.ts +25 -0
  76. package/src/lib/supervise-the-run/internal/start-watching-the-supervisor.ts +6 -0
  77. package/src/lib/supervise-the-run/internal/supervise-run.ts +155 -0
  78. package/src/lib/supervise-the-run/internal/talk-to-the-supervisor.ts +53 -0
  79. package/src/lib/supervise-the-run/internal/watch-the-supervisor.ts +61 -0
  80. package/src/lib/task/index.ts +5 -0
  81. package/src/lib/task/internal/make-tasks.ts +82 -0
  82. package/src/lib/task/internal/run-command.ts +194 -0
  83. package/src/lib/task/internal/run-root-task.ts +263 -0
  84. package/src/lib/task/internal/run-task-lists.ts +51 -0
  85. package/src/lib/task/internal/trust-output-mode-names.ts +18 -0
  86. package/src/lib/task/internal/trust-task-results.ts +35 -0
  87. package/src/lib/task/internal/use-task-services.ts +4 -0
  88. package/src/lib/worker-thread/index.ts +3 -0
  89. package/src/lib/worker-thread/internal/answer-main-thread-messages.ts +141 -0
  90. package/src/lib/worker-thread/internal/describe-task-body-result.ts +91 -0
  91. package/src/lib/worker-thread/internal/name-config-key-not-set.ts +57 -0
  92. package/src/lib/worker-thread/internal/run-task-body.ts +117 -0
  93. package/src/lib/worker-thread/internal/serve-on-the-parent-port.ts +63 -0
  94. package/src/lib/worker-thread/internal/start-worker-thread.ts +41 -0
  95. package/src/lib/worker-thread/lib/provide-task-services/index.ts +1 -0
  96. package/src/lib/worker-thread/lib/provide-task-services/internal/make-task-logger.ts +34 -0
  97. package/src/lib/worker-thread/lib/provide-task-services/internal/provide-task-services.ts +86 -0
  98. package/src/lib/worker-thread/lib/provide-task-services/internal/read-standard-input-from-main-thread.ts +58 -0
  99. package/src/lib/worker-thread/lib/provide-task-services/internal/resolve-paths-from-package-directory.ts +61 -0
  100. package/src/lib/worker-thread/lib/provide-task-services/internal/run-commands-in-package-directory.ts +24 -0
  101. package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-commands-with-bun.ts +545 -0
  102. package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-reported-process.ts +151 -0
  103. package/src/lib/worker-thread/shared/await-main-thread-answers.ts +76 -0
  104. package/src/lib/worker-thread/shared/decode-task-result-json.ts +114 -0
  105. package/src/lib/worker-thread/shared/define-task-body-record.ts +11 -0
  106. package/src/lib/worker-thread/shared/describe-issue-input.ts +52 -0
  107. package/src/lib/worker-thread/shared/plain-object.ts +8 -0
  108. package/src/lib/worker-thread/shared/provide-worker-state.ts +160 -0
  109. package/src/lib/worker-thread/shared/worker-thread-errors.ts +41 -0
  110. package/src/shared/attribute-reported-failures.ts +25 -0
  111. package/src/shared/claim-processes-to-report.ts +22 -0
  112. package/src/shared/command-errors.ts +44 -0
  113. package/src/shared/complete-discarded-calls-once-sent.ts +12 -0
  114. package/src/shared/cut-to-graphemes.ts +12 -0
  115. package/src/shared/environment-config-errors.ts +74 -0
  116. package/src/shared/error-fields.ts +13 -0
  117. package/src/shared/find-bun-loader-for-script.ts +32 -0
  118. package/src/shared/find-node-modules-package-directory.ts +35 -0
  119. package/src/shared/find-user-locations-in-stack-traces.ts +64 -0
  120. package/src/shared/format-error-lines.ts +53 -0
  121. package/src/shared/format-task-lines.ts +21 -0
  122. package/src/shared/hold-task-logs.ts +24 -0
  123. package/src/shared/invariant-errors.ts +28 -0
  124. package/src/shared/mark-task-worker-thread-contexts.ts +29 -0
  125. package/src/shared/module-graph-errors.ts +49 -0
  126. package/src/shared/name-config-key.ts +42 -0
  127. package/src/shared/names-from-package-json.ts +7 -0
  128. package/src/shared/print-to-standard-error.ts +48 -0
  129. package/src/shared/process-errors.ts +191 -0
  130. package/src/shared/read-environment-config.ts +239 -0
  131. package/src/shared/record-tasks-made.ts +10 -0
  132. package/src/shared/report-cause.ts +140 -0
  133. package/src/shared/report-errors.ts +121 -0
  134. package/src/shared/resolve-task-options.ts +20 -0
  135. package/src/shared/rpc.ts +161 -0
  136. package/src/shared/run-in-child-fiber-and-join.ts +16 -0
  137. package/src/shared/run-marker.ts +7 -0
  138. package/src/shared/save-standard-stream-flags.ts +57 -0
  139. package/src/shared/select-cause-reasons.ts +14 -0
  140. package/src/shared/signal-run-called-twice.ts +11 -0
  141. package/src/shared/standard-input-streams.ts +41 -0
  142. package/src/shared/stop-signals.ts +41 -0
  143. package/src/shared/task-errors.ts +224 -0
  144. package/src/shared/task-package.ts +5 -0
  145. package/src/shared/task-results.ts +55 -0
  146. package/src/shared/task-services.ts +30 -0
  147. package/src/shared/write-trace-file.ts +112 -0
@@ -0,0 +1,607 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { Worker } from "node:worker_threads";
3
+
4
+ import {
5
+ Cause,
6
+ Context,
7
+ Deferred,
8
+ Duration,
9
+ Effect,
10
+ Exit,
11
+ FiberSet,
12
+ Layer,
13
+ Option,
14
+ Queue,
15
+ Ref,
16
+ Schema,
17
+ Scope,
18
+ Semaphore,
19
+ Stream,
20
+ Tracer,
21
+ } from "effect";
22
+ import { RpcClient, RpcSerialization, RpcServer, RpcWorker } from "effect/rpc";
23
+ import type { RpcClientError, RpcMessage } from "effect/rpc";
24
+ import { Worker as EffectWorker } from "effect/workers";
25
+
26
+ import type { ProcessIdentity } from "../../../shared/claim-processes-to-report.ts";
27
+ import { findUserLocationInStack } from "../../../shared/find-user-locations-in-stack-traces.ts";
28
+ import { dieOnInvariantFailure } from "../../../shared/invariant-errors.ts";
29
+ import { TASK_TRACE_LABEL_PREFIX } from "../../../shared/names-from-package-json.ts";
30
+ import { UncaughtWorkerError } from "../../../shared/process-errors.ts";
31
+ import { bunArguments, commandLineArguments } from "../../../shared/read-environment-config.ts";
32
+ import { describeCauseMessage, ErrorStack } from "../../../shared/report-errors.ts";
33
+ import {
34
+ MainThreadRpcs,
35
+ MessageFromMainThreadRpcClient,
36
+ TASK_EVENT_FIELDS,
37
+ TaskWorkerRpcs,
38
+ WorkerInitialMessage,
39
+ } from "../../../shared/rpc.ts";
40
+ import type { StandardInputRead, StartTaskFiber, WorkerData } from "../../../shared/rpc.ts";
41
+ import type { TaskRequestFailure } from "../../../shared/task-errors.ts";
42
+ import { TASK_PACKAGE } from "../../../shared/task-package.ts";
43
+ import { TraceFile } from "../../../shared/write-trace-file.ts";
44
+ import { ModuleGraph } from "../lib/find-code-each-task-depends-on/index.ts";
45
+ import { DiscoveredTasks } from "./provide-discovered-tasks.ts";
46
+ import { RunRootTaskConfig } from "./provide-run-root-task-config.ts";
47
+ import { StopState } from "./provide-stop-state.ts";
48
+
49
+ const WORKER_EXIT_GRACE_SECONDS = 2;
50
+
51
+ const WORKER_EXIT_GRACE = Duration.seconds(WORKER_EXIT_GRACE_SECONDS);
52
+
53
+ const FORCED_STOP_REASON = "without waiting for it to exit because the stop was forced";
54
+
55
+ export const FromWorker = Schema.TaggedUnion({
56
+ ...TASK_EVENT_FIELDS,
57
+ TaskFiberRequestedTask: {
58
+ dependencyRequestId: Schema.Int,
59
+ taskName: Schema.String,
60
+ dependencyTaskName: Schema.String,
61
+ },
62
+ TaskFiberCancelledRequest: { taskName: Schema.String, dependencyRequestId: Schema.Int },
63
+ TaskFiberRequestedStandardInput: { taskName: Schema.String, readId: Schema.Int },
64
+ TaskFiberStartedProcess: {
65
+ taskName: Schema.String,
66
+ recordId: Schema.Int,
67
+ processId: Schema.Int,
68
+ startTime: Schema.NullOr(Schema.String),
69
+ },
70
+ TaskFiberClaimingProcessReports: {
71
+ taskName: Schema.String,
72
+ claimId: Schema.Int,
73
+ processes: Schema.Array(Schema.Struct({ processId: Schema.Int, startTime: Schema.String })),
74
+ },
75
+ });
76
+
77
+ const decodeMessageFromMainThreadRpcClient = Schema.decodeUnknownExit(
78
+ MessageFromMainThreadRpcClient,
79
+ );
80
+
81
+ const holdMessagesUntilListened = (thread: Worker) => {
82
+ const inbox = {
83
+ heldMessages: new Array<EffectWorker.PlatformMessage>(),
84
+ deliver: (message: EffectWorker.PlatformMessage) => {
85
+ inbox.heldMessages.push(message);
86
+ },
87
+ };
88
+ thread.on("message", (message: EffectWorker.PlatformMessage) => {
89
+ inbox.deliver(message);
90
+ });
91
+ return {
92
+ postMessage: (...posting: Parameters<Worker["postMessage"]>) => {
93
+ thread.postMessage(...posting);
94
+ },
95
+ listen: (emit: (message: EffectWorker.PlatformMessage) => void) => {
96
+ inbox.deliver = emit;
97
+ for (const message of inbox.heldMessages.splice(0)) {
98
+ emit(message);
99
+ }
100
+ },
101
+ };
102
+ };
103
+
104
+ const workerThreadPlatform = EffectWorker.makePlatform<
105
+ ReturnType<typeof holdMessagesUntilListened>
106
+ >()({
107
+ setup: ({ worker }) => Effect.succeed(worker),
108
+ listen: ({ port, emit }) =>
109
+ Effect.sync(() => {
110
+ port.listen(emit);
111
+ }),
112
+ });
113
+
114
+ export const WorkerEvent = Schema.TaggedUnion({
115
+ MessageReceived: { workerId: Schema.Int, message: FromWorker },
116
+ MessageUnreadable: { workerId: Schema.Int, reason: Schema.String },
117
+ ExitedWithCode: {
118
+ workerId: Schema.Int,
119
+ exitCode: Schema.Int,
120
+ terminationRequested: Schema.Boolean,
121
+ uncaughtError: Schema.NullOr(UncaughtWorkerError),
122
+ },
123
+ });
124
+
125
+ const answerPendingRequest = <Success, Failure>(
126
+ pendingAnswers: Map<number, Deferred.Deferred<Success, Failure>>,
127
+ answerId: number,
128
+ exit: Exit.Exit<Success, Failure>,
129
+ ) =>
130
+ Effect.sync(() => {
131
+ const pendingAnswer = pendingAnswers.get(answerId);
132
+ if (pendingAnswer !== undefined) {
133
+ Deferred.doneUnsafe(pendingAnswer, exit);
134
+ }
135
+ });
136
+
137
+ const makeWorkerPool = Effect.gen(function* () {
138
+ const shutdown = yield* StopState;
139
+ const runRootTaskConfig = yield* RunRootTaskConfig;
140
+ const moduleGraph = yield* ModuleGraph;
141
+ const traceFile = yield* TraceFile;
142
+ const parentSpan = yield* Tracer.ParentSpan;
143
+ const { tasksByName } = yield* DiscoveredTasks;
144
+ const taskFilePaths =
145
+ tasksByName === null
146
+ ? []
147
+ : Array.from(tasksByName.values(), (discoveredTask) => discoveredTask.filePath);
148
+ const workerLimit = Math.min(runRootTaskConfig.workerCount, taskFilePaths.length);
149
+ const workersById = new Map<
150
+ number,
151
+ {
152
+ readonly thread: Worker;
153
+ readonly client: RpcClient.FromGroup<typeof TaskWorkerRpcs, RpcClientError.RpcClientError>;
154
+ readonly exitDeferred: Deferred.Deferred<void>;
155
+ terminationRequested: boolean;
156
+ terminateCalled: boolean;
157
+ uncaughtError: Option.Option<typeof UncaughtWorkerError.Type>;
158
+ }
159
+ >();
160
+ const workerSelectionSemaphore = yield* Semaphore.make(1);
161
+ const workerEvents = yield* Queue.unbounded<typeof WorkerEvent.Type, Cause.Done>();
162
+ const workerConnectionsScope = yield* Scope.fork(yield* Effect.scope);
163
+ const callsToWorkers = yield* FiberSet.make().pipe(Scope.provide(workerConnectionsScope));
164
+ const nextAnswerId = yield* Ref.make(0);
165
+ const pendingDependencyResults = new Map<
166
+ number,
167
+ Deferred.Deferred<{ readonly value?: Schema.Json }, typeof TaskRequestFailure.Type>
168
+ >();
169
+ const pendingStandardInputReads = new Map<
170
+ number,
171
+ Deferred.Deferred<typeof StandardInputRead.Type>
172
+ >();
173
+ const pendingProcessRecords = new Map<number, Deferred.Deferred<void>>();
174
+ const pendingReportClaims = new Map<
175
+ number,
176
+ Deferred.Deferred<readonly (typeof ProcessIdentity.Type)[]>
177
+ >();
178
+ const receiveMessage = (workerId: number, message: typeof FromWorker.Type) => {
179
+ Queue.offerUnsafe(workerEvents, WorkerEvent.cases.MessageReceived.make({ workerId, message }));
180
+ };
181
+ const reportAndAwaitAnswer = <Success, Failure>(
182
+ workerId: number,
183
+ pendingAnswers: Map<number, Deferred.Deferred<Success, Failure>>,
184
+ makeMessage: (answerId: number) => typeof FromWorker.Type,
185
+ makeCancelledMessage: ((answerId: number) => typeof FromWorker.Type) | null,
186
+ ) =>
187
+ Effect.gen(function* () {
188
+ const answerId = yield* Ref.getAndUpdate(nextAnswerId, (id) => id + 1);
189
+ const answer = Deferred.makeUnsafe<Success, Failure>();
190
+ pendingAnswers.set(answerId, answer);
191
+ receiveMessage(workerId, makeMessage(answerId));
192
+ return yield* Deferred.await(answer).pipe(
193
+ Effect.onInterrupt(() =>
194
+ Effect.sync(() => {
195
+ if (makeCancelledMessage !== null) {
196
+ receiveMessage(workerId, makeCancelledMessage(answerId));
197
+ }
198
+ }),
199
+ ),
200
+ Effect.ensuring(
201
+ Effect.sync(() => {
202
+ pendingAnswers.delete(answerId);
203
+ }),
204
+ ),
205
+ );
206
+ });
207
+ const makeWorkerRpcServerProtocol = (workerId: number, workerPort: MessagePort) =>
208
+ RpcServer.Protocol.make((writeRequest) =>
209
+ Effect.gen(function* () {
210
+ const runInPoolScope = yield* FiberSet.makeRuntime<never, void, never>();
211
+ const onMessage = (event: { readonly data: unknown }) => {
212
+ Exit.match(decodeMessageFromMainThreadRpcClient(event.data), {
213
+ onSuccess: (request) => {
214
+ runInPoolScope(writeRequest(0, request));
215
+ },
216
+ onFailure: (cause) => {
217
+ Queue.offerUnsafe(
218
+ workerEvents,
219
+ WorkerEvent.cases.MessageUnreadable.make({ workerId, reason: Cause.pretty(cause) }),
220
+ );
221
+ },
222
+ });
223
+ };
224
+ yield* Effect.acquireRelease(
225
+ Effect.sync(() => {
226
+ workerPort.addEventListener("message", onMessage);
227
+ workerPort.start();
228
+ }),
229
+ () =>
230
+ Effect.sync(() => {
231
+ workerPort.removeEventListener("message", onMessage);
232
+ workerPort.close();
233
+ }),
234
+ );
235
+ const workerRpcServerProtocol = {
236
+ disconnects: yield* Queue.make<number>(),
237
+ send: (_clientId: number, response: RpcMessage.FromServerEncoded) =>
238
+ Effect.sync(() => {
239
+ workerPort.postMessage(response);
240
+ }),
241
+ end: () => Effect.void,
242
+ clientIds: Effect.succeed(new Set([0])),
243
+ initialMessage: Effect.succeedNone,
244
+ supportsAck: false,
245
+ supportsTransferables: false,
246
+ supportsSpanPropagation: false,
247
+ supportsNotifications: false,
248
+ codecFor: RpcSerialization.json.codecFor,
249
+ };
250
+ return workerRpcServerProtocol;
251
+ }),
252
+ );
253
+ const serveWorker = (workerId: number, workerPort: MessagePort) =>
254
+ Effect.gen(function* () {
255
+ const handlers = yield* MainThreadRpcs.toHandlers({
256
+ ReportTaskEvent: (event) =>
257
+ Effect.sync(() => {
258
+ receiveMessage(workerId, event);
259
+ }),
260
+ RequestTask: (requested) =>
261
+ reportAndAwaitAnswer(
262
+ workerId,
263
+ pendingDependencyResults,
264
+ (dependencyRequestId) =>
265
+ FromWorker.cases.TaskFiberRequestedTask.make({ ...requested, dependencyRequestId }),
266
+ (dependencyRequestId) =>
267
+ FromWorker.cases.TaskFiberCancelledRequest.make({
268
+ taskName: requested.taskName,
269
+ dependencyRequestId,
270
+ }),
271
+ ),
272
+ ReadStandardInput: (requested) =>
273
+ reportAndAwaitAnswer(
274
+ workerId,
275
+ pendingStandardInputReads,
276
+ (readId) =>
277
+ FromWorker.cases.TaskFiberRequestedStandardInput.make({
278
+ taskName: requested.taskName,
279
+ readId,
280
+ }),
281
+ null,
282
+ ),
283
+ RecordStartedProcess: (started) =>
284
+ reportAndAwaitAnswer(
285
+ workerId,
286
+ pendingProcessRecords,
287
+ (recordId) => FromWorker.cases.TaskFiberStartedProcess.make({ ...started, recordId }),
288
+ null,
289
+ ),
290
+ ClaimProcessReports: (claiming) =>
291
+ reportAndAwaitAnswer(
292
+ workerId,
293
+ pendingReportClaims,
294
+ (claimId) =>
295
+ FromWorker.cases.TaskFiberClaimingProcessReports.make({ ...claiming, claimId }),
296
+ null,
297
+ ),
298
+ });
299
+ yield* RpcServer.make(MainThreadRpcs, { disableTracing: true }).pipe(
300
+ Effect.provideContext(handlers),
301
+ Effect.provideServiceEffect(
302
+ RpcServer.Protocol,
303
+ makeWorkerRpcServerProtocol(workerId, workerPort),
304
+ ),
305
+ Effect.interruptible,
306
+ Effect.forkScoped,
307
+ Scope.provide(workerConnectionsScope),
308
+ );
309
+ });
310
+ const connectToWorker = (
311
+ thread: ReturnType<typeof holdMessagesUntilListened>,
312
+ mainThreadPort: MessagePort,
313
+ ) =>
314
+ RpcClient.make(TaskWorkerRpcs, { disableTracing: true }).pipe(
315
+ Effect.provideServiceEffect(
316
+ RpcClient.Protocol,
317
+ RpcClient.makeProtocolWorker({ size: 1, concurrency: Number.POSITIVE_INFINITY }),
318
+ ),
319
+ Effect.provideService(EffectWorker.WorkerPlatform, workerThreadPlatform),
320
+ Effect.provideService(EffectWorker.Spawner, () => thread),
321
+ Effect.provideService(
322
+ RpcWorker.InitialMessage,
323
+ RpcWorker.makeInitialMessage(WorkerInitialMessage, Effect.succeed({ mainThreadPort })).pipe(
324
+ dieOnInvariantFailure("worker startup", "the main thread's RPC port can be handed over"),
325
+ ),
326
+ ),
327
+ Scope.provide(workerConnectionsScope),
328
+ dieOnInvariantFailure("worker startup", "a started worker can be called over RPC"),
329
+ );
330
+
331
+ const startWorker = (
332
+ workerId: number,
333
+ rewrittenFiles: (typeof WorkerData.Type)["rewrittenFiles"],
334
+ ) =>
335
+ Effect.gen(function* () {
336
+ const workerData = {
337
+ workerId,
338
+ traceFilePath: traceFile.filePath,
339
+ projectDirectoryPath: runRootTaskConfig.rootTaskPackageJsonDirectoryPath,
340
+ rewrittenFiles,
341
+ taskLogHold: shutdown.taskLogHold,
342
+ runMarker: runRootTaskConfig.runMarker,
343
+ } satisfies typeof WorkerData.Type;
344
+ const rpcChannel = new MessageChannel();
345
+ yield* serveWorker(workerId, rpcChannel.port1);
346
+ const thread = new Worker(new URL("../../worker-thread/index.ts", import.meta.url), {
347
+ workerData,
348
+ argv: yield* commandLineArguments,
349
+ execArgv: yield* bunArguments,
350
+ ...(Object.keys(rewrittenFiles).length === 0
351
+ ? {}
352
+ : {
353
+ preload: [
354
+ fileURLToPath(
355
+ new URL(
356
+ "../../preload-rewritten-imports-into-each-worker/index.ts",
357
+ import.meta.url,
358
+ ),
359
+ ),
360
+ ],
361
+ }),
362
+ });
363
+ const threadMessages = holdMessagesUntilListened(thread);
364
+ const workerThread = {
365
+ thread,
366
+ client: yield* connectToWorker(threadMessages, rpcChannel.port2),
367
+ exitDeferred: yield* Deferred.make<void>(),
368
+ terminationRequested: false,
369
+ terminateCalled: false,
370
+ uncaughtError: Option.none(),
371
+ } satisfies Parameters<typeof workersById.set>[1];
372
+ workersById.set(workerId, workerThread);
373
+ workerThread.thread.on("error", (error) => {
374
+ workerThread.uncaughtError = Option.some({
375
+ message: describeCauseMessage(error),
376
+ location: Option.getOrNull(
377
+ Option.flatMap(Schema.decodeUnknownOption(ErrorStack)(error), (withStack) =>
378
+ Option.fromUndefinedOr(findUserLocationInStack(withStack.stack)),
379
+ ),
380
+ ),
381
+ });
382
+ });
383
+ workerThread.thread.on("exit", (exitCode) => {
384
+ Deferred.doneUnsafe(workerThread.exitDeferred, Effect.void);
385
+ Queue.offerUnsafe(
386
+ workerEvents,
387
+ WorkerEvent.cases.ExitedWithCode.make({
388
+ workerId,
389
+ exitCode,
390
+ terminationRequested: workerThread.terminationRequested,
391
+ uncaughtError: Option.getOrNull(workerThread.uncaughtError),
392
+ }),
393
+ );
394
+ });
395
+ });
396
+ const readRewrittenWorkerImports = yield* Effect.cached(
397
+ moduleGraph.rewriteBareImports([
398
+ fileURLToPath(new URL("../../worker-thread/index.ts", import.meta.url)),
399
+ ...taskFilePaths,
400
+ ]),
401
+ );
402
+ const startNextWorker = Effect.gen(function* () {
403
+ const workerId = workersById.size;
404
+ const rewrittenWorkerImports = yield* readRewrittenWorkerImports;
405
+ if (yield* shutdown.isStopping) {
406
+ return yield* Effect.interrupt;
407
+ }
408
+ yield* startWorker(workerId, rewrittenWorkerImports);
409
+ return workerId;
410
+ });
411
+ const terminateWorker = (
412
+ workerId: number,
413
+ workerThread: Parameters<typeof workersById.set>[1],
414
+ reason: string,
415
+ ) =>
416
+ Effect.gen(function* () {
417
+ const isWorkerEnding =
418
+ workerThread.terminateCalled || Deferred.isDoneUnsafe(workerThread.exitDeferred);
419
+ if (isWorkerEnding) {
420
+ return;
421
+ }
422
+ workerThread.terminateCalled = true;
423
+ workerThread.terminationRequested = true;
424
+ yield* Effect.logInfo(`worker terminated ${reason}`).pipe(
425
+ Effect.annotateLogs(`${TASK_TRACE_LABEL_PREFIX}.worker`, workerId),
426
+ );
427
+ void workerThread.thread.terminate();
428
+ }).pipe(Effect.uninterruptible);
429
+ const requestExitOfAllWorkers = Effect.forEach(
430
+ workersById.values(),
431
+ (workerThread) =>
432
+ Effect.gen(function* () {
433
+ const isWorkerLeaving =
434
+ workerThread.terminationRequested || Deferred.isDoneUnsafe(workerThread.exitDeferred);
435
+ if (isWorkerLeaving) {
436
+ return;
437
+ }
438
+ workerThread.terminationRequested = true;
439
+ yield* FiberSet.run(
440
+ callsToWorkers,
441
+ workerThread.client
442
+ .ExitWorker()
443
+ .pipe(Effect.catchTag("RpcClientError", () => Effect.void)),
444
+ );
445
+ }),
446
+ { discard: true },
447
+ );
448
+ const terminateAllWorkers = Effect.forEach(
449
+ workersById,
450
+ ([workerId, workerThread]) => terminateWorker(workerId, workerThread, FORCED_STOP_REASON),
451
+ { discard: true },
452
+ );
453
+ const awaitWorkerShutdown = Effect.forEach(
454
+ workersById,
455
+ ([workerId, workerThread]) =>
456
+ Effect.gen(function* () {
457
+ const exited = yield* Deferred.await(workerThread.exitDeferred).pipe(
458
+ Effect.timeoutOption(WORKER_EXIT_GRACE),
459
+ Effect.raceFirst(shutdown.awaitForcedStop.pipe(Effect.as(Option.none()))),
460
+ );
461
+ if (Option.isSome(exited)) {
462
+ return;
463
+ }
464
+ yield* terminateWorker(
465
+ workerId,
466
+ workerThread,
467
+ (yield* shutdown.isForced)
468
+ ? FORCED_STOP_REASON
469
+ : `because it did not exit within ${Duration.format(WORKER_EXIT_GRACE)} of being asked to`,
470
+ );
471
+ yield* Deferred.await(workerThread.exitDeferred);
472
+ }),
473
+ { concurrency: "unbounded", discard: true },
474
+ ).pipe(Effect.andThen(Queue.end(workerEvents)));
475
+ const findReachableWorker = (workerId: number, rpcTag: string) =>
476
+ Effect.gen(function* () {
477
+ const workerThread = yield* Effect.fromNullishOr(workersById.get(workerId)).pipe(
478
+ dieOnInvariantFailure("worker pool", "messages go to a started worker"),
479
+ );
480
+ const isWorkerGone =
481
+ workerThread.terminationRequested || Deferred.isDoneUnsafe(workerThread.exitDeferred);
482
+ if (isWorkerGone) {
483
+ yield* Effect.logDebug(
484
+ "message not sent because its worker is gone, and that worker's exit ends its tasks",
485
+ ).pipe(
486
+ Effect.annotateLogs({
487
+ [`${TASK_TRACE_LABEL_PREFIX}.worker`]: workerId,
488
+ [`${TASK_TRACE_LABEL_PREFIX}.message`]: rpcTag,
489
+ }),
490
+ );
491
+ return null;
492
+ }
493
+ return workerThread;
494
+ });
495
+ const callWorker = (
496
+ workerId: number,
497
+ rpcTag: string,
498
+ call: (
499
+ client: RpcClient.FromGroup<typeof TaskWorkerRpcs, RpcClientError.RpcClientError>,
500
+ ) => Effect.Effect<void, RpcClientError.RpcClientError>,
501
+ ) =>
502
+ Effect.gen(function* () {
503
+ const workerThread = yield* findReachableWorker(workerId, rpcTag);
504
+ if (workerThread === null) {
505
+ return false;
506
+ }
507
+ yield* FiberSet.run(
508
+ callsToWorkers,
509
+ call(workerThread.client).pipe(Effect.catchTag("RpcClientError", () => Effect.void)),
510
+ );
511
+ return true;
512
+ });
513
+ yield* Effect.addFinalizer(() => Effect.andThen(requestExitOfAllWorkers, awaitWorkerShutdown));
514
+ return {
515
+ events: Stream.fromQueue(workerEvents).pipe(
516
+ Stream.provideService(Tracer.ParentSpan, parentSpan),
517
+ ),
518
+ selectWorkerIdForTask: (
519
+ readWorkerLoad: (workerId: number) => {
520
+ readonly occupyingTaskCount: number;
521
+ readonly servingTaskCount: number;
522
+ },
523
+ recordAssignment: (workerId: number) => Effect.Effect<void>,
524
+ ) =>
525
+ Effect.gen(function* () {
526
+ if (yield* shutdown.isStopping) {
527
+ return yield* Effect.interrupt;
528
+ }
529
+ const usableWorkerIds = [...workersById]
530
+ .filter(
531
+ ([, workerThread]) =>
532
+ !workerThread.terminationRequested &&
533
+ !Deferred.isDoneUnsafe(workerThread.exitDeferred),
534
+ )
535
+ .map(([workerId]) => workerId);
536
+ const usableWorkerLoads = usableWorkerIds.map(
537
+ (usableWorkerId) => [usableWorkerId, readWorkerLoad(usableWorkerId)] as const,
538
+ );
539
+ const idleWorkerId = usableWorkerLoads.find(
540
+ ([, load]) => load.occupyingTaskCount === 0 && load.servingTaskCount === 0,
541
+ )?.[0];
542
+ const workerId =
543
+ idleWorkerId ??
544
+ (workersById.size < workerLimit
545
+ ? yield* startNextWorker
546
+ : usableWorkerLoads.toSorted(
547
+ ([, left], [, right]) =>
548
+ left.occupyingTaskCount - right.occupyingTaskCount ||
549
+ left.servingTaskCount - right.servingTaskCount,
550
+ )[0]?.[0]);
551
+ if (workerId === undefined) {
552
+ yield* shutdown.awaitFailure;
553
+ return yield* Effect.interrupt;
554
+ }
555
+ yield* recordAssignment(workerId);
556
+ return workerId;
557
+ }).pipe(workerSelectionSemaphore.withPermits(1)),
558
+ startTaskFiber: (workerId: number, start: typeof StartTaskFiber.Type) =>
559
+ callWorker(workerId, "StartTaskFiber", (client) => client.StartTaskFiber(start)),
560
+ stopTaskFiber: (workerId: number, taskName: string) =>
561
+ callWorker(workerId, "StopTaskFiber", (client) => client.StopTaskFiber({ taskName })),
562
+ answerDependencyRequest: (
563
+ dependencyRequestId: number,
564
+ exit: Exit.Exit<Schema.Json | undefined, typeof TaskRequestFailure.Type>,
565
+ ) =>
566
+ answerPendingRequest(
567
+ pendingDependencyResults,
568
+ dependencyRequestId,
569
+ Exit.map(exit, (value) => (value === undefined ? {} : { value })),
570
+ ),
571
+ answerStandardInputRead: (readId: number, read: typeof StandardInputRead.Type) =>
572
+ answerPendingRequest(pendingStandardInputReads, readId, Exit.succeed(read)),
573
+ acknowledgeProcessRecorded: (recordId: number) =>
574
+ answerPendingRequest(pendingProcessRecords, recordId, Exit.void),
575
+ answerReportClaim: (claimId: number, claimed: readonly (typeof ProcessIdentity.Type)[]) =>
576
+ answerPendingRequest(pendingReportClaims, claimId, Exit.succeed(claimed)),
577
+ awaitAnswer: (workerId: number) =>
578
+ Effect.gen(function* () {
579
+ const reachableWorker = yield* findReachableWorker(workerId, "AnswerIfResponsive");
580
+ if (reachableWorker === null) {
581
+ return "gone" as const;
582
+ }
583
+ return yield* reachableWorker.client.AnswerIfResponsive().pipe(
584
+ Effect.as("answered" as const),
585
+ Effect.catchTag("RpcClientError", () => Effect.succeed("gone" as const)),
586
+ Effect.raceFirst(
587
+ Deferred.await(reachableWorker.exitDeferred).pipe(Effect.as("gone" as const)),
588
+ ),
589
+ );
590
+ }),
591
+ requestWorkerTermination: (workerId: number, reason: string) =>
592
+ Effect.fromNullishOr(workersById.get(workerId)).pipe(
593
+ dieOnInvariantFailure("worker pool", "a worker whose termination is requested was started"),
594
+ Effect.flatMap((workerThread) => terminateWorker(workerId, workerThread, reason)),
595
+ ),
596
+ requestExitOfAllWorkers,
597
+ terminateAllWorkers,
598
+ awaitWorkerShutdown,
599
+ startedWorkerCount: Effect.sync(() => workersById.size),
600
+ };
601
+ });
602
+
603
+ export class WorkerPool extends Context.Service<
604
+ WorkerPool,
605
+ Effect.Success<typeof makeWorkerPool>
606
+ >()(`${TASK_PACKAGE.name}/WorkerPool`) {}
607
+ export const WorkerPoolLive = Layer.effect(WorkerPool, makeWorkerPool);
@@ -0,0 +1,3 @@
1
+ const BARE_PACKAGE = /^(?![./]|[a-z]+:)(?<packageName>(?:@[^/@]+\/)?[^/@]+)/u;
2
+
3
+ export const readBarePackageName = (specifier: string) => BARE_PACKAGE.exec(specifier)?.[1];