@sshkeda/task 0.0.0-stage → 1.0.1

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,151 @@
1
+ import { Duration, Effect, Deferred, Exit, PlatformError } from "effect";
2
+ import { ChildProcessSpawner } from "effect/process";
3
+ import type { ChildProcess } from "effect/process";
4
+
5
+ import { TASK_TRACE_LABEL_PREFIX } from "../../../../../shared/names-from-package-json.ts";
6
+ import { TaskEvent } from "../../../../../shared/rpc.ts";
7
+ import type { StartTaskFiber } from "../../../../../shared/rpc.ts";
8
+ import {
9
+ forgetEndedProcessGroups,
10
+ killTaskProcessGroups,
11
+ readProcessUnlessGone,
12
+ recordLeaderExit,
13
+ startedProcessGroup,
14
+ } from "../../../../kill-leftover-processes/index.ts";
15
+ import type { makeTaskBodyRecord } from "../../../shared/define-task-body-record.ts";
16
+ import { WorkerState, reportToMainThread } from "../../../shared/provide-worker-state.ts";
17
+ import { refuseStandardInputOutsideRootTask } from "./read-standard-input-from-main-thread.ts";
18
+ import { formatProgramNames, spawnWithBun } from "./spawn-commands-with-bun.ts";
19
+
20
+ const MICROSECONDS_PER_MILLISECOND = 1000;
21
+
22
+ export const spawnReportedProcess = Effect.fn("spawnReportedProcess")(function* (
23
+ startMessage: typeof StartTaskFiber.Type,
24
+ body: ReturnType<typeof makeTaskBodyRecord>,
25
+ command: ChildProcess.Command,
26
+ ) {
27
+ const workerState = yield* WorkerState;
28
+ const spanEndedDeferred = yield* Deferred.make<void>();
29
+ const spawnedDeferred = yield* Deferred.make<
30
+ ChildProcessSpawner.ChildProcessHandle,
31
+ PlatformError.PlatformError
32
+ >();
33
+ const leaderExitsSeenBeforeRecord = new Map<number, string | null>();
34
+ const onLeaderExited = (processId: number, latestMemberStartTime: string | null) =>
35
+ Effect.sync(() => {
36
+ leaderExitsSeenBeforeRecord.set(processId, latestMemberStartTime);
37
+ recordLeaderExit(body.startedProcessGroups, processId, latestMemberStartTime);
38
+ });
39
+ const killMembersLeftByLeader = (processId: number, latestMemberStartTime: string) =>
40
+ killTaskProcessGroups(
41
+ new Map([
42
+ [
43
+ processId,
44
+ startedProcessGroup(
45
+ startMessage.taskName,
46
+ body.startedProcessGroups.get(processId)?.leaderStartTime ?? null,
47
+ latestMemberStartTime,
48
+ ),
49
+ ],
50
+ ]),
51
+ startMessage.taskName,
52
+ (processes) => workerState.claimProcessesToReport(startMessage.taskName, processes),
53
+ ).pipe(
54
+ workerState.provideRuntimeServices,
55
+ Effect.flatMap(forgetEndedProcessGroups(body.startedProcessGroups)),
56
+ );
57
+ yield* spawnWithBun(
58
+ command,
59
+ refuseStandardInputOutsideRootTask(startMessage),
60
+ startMessage.taskPackageDirectoryPath,
61
+ Duration.millis(startMessage.cleanupMilliseconds),
62
+ {
63
+ onLeaderExited,
64
+ killMembersLeftByLeader,
65
+ },
66
+ ).pipe(
67
+ Effect.tap((spawnedProcess) =>
68
+ readProcessUnlessGone(startMessage.taskName, spawnedProcess.handle.pid).pipe(
69
+ workerState.provideRuntimeServices,
70
+ Effect.mapError((readFailure) =>
71
+ PlatformError.systemError({
72
+ _tag: "Unknown",
73
+ module: "ChildProcess",
74
+ method: "spawn",
75
+ pathOrDescriptor: formatProgramNames(command),
76
+ description: readFailure.message,
77
+ cause: readFailure,
78
+ }),
79
+ ),
80
+ Effect.map((leader) => leader?.startTime ?? null),
81
+ Effect.tap((leaderStartTime) =>
82
+ Effect.sync(() => {
83
+ const processId = spawnedProcess.handle.pid;
84
+ body.startedProcessGroups.set(
85
+ processId,
86
+ startedProcessGroup(startMessage.taskName, leaderStartTime, null),
87
+ );
88
+ if (leaderExitsSeenBeforeRecord.has(processId)) {
89
+ recordLeaderExit(
90
+ body.startedProcessGroups,
91
+ processId,
92
+ leaderExitsSeenBeforeRecord.get(processId) ?? null,
93
+ );
94
+ }
95
+ }),
96
+ ),
97
+ Effect.flatMap((leaderStartTime) =>
98
+ workerState.reportStartedProcessAndAwaitRecord(
99
+ startMessage.taskName,
100
+ spawnedProcess.handle.pid,
101
+ leaderStartTime,
102
+ ),
103
+ ),
104
+ ),
105
+ ),
106
+ Effect.tap((spawnedProcess) =>
107
+ Effect.annotateCurrentSpan("process.pid", spawnedProcess.handle.pid),
108
+ ),
109
+ Effect.onExit((spawnExit) =>
110
+ Deferred.done(
111
+ spawnedDeferred,
112
+ Exit.map(spawnExit, (spawnedProcess) => spawnedProcess.handle),
113
+ ),
114
+ ),
115
+ Effect.flatMap((spawnedProcess) =>
116
+ spawnedProcess.exited.pipe(
117
+ Effect.tap((processExit) =>
118
+ reportToMainThread(
119
+ TaskEvent.cases.TaskFiberProcessLeaderExited.make({
120
+ taskName: startMessage.taskName,
121
+ processId: spawnedProcess.handle.pid,
122
+ latestMemberStartTime: processExit.latestMemberStartTime,
123
+ }),
124
+ ),
125
+ ),
126
+ Effect.tap((processExit) =>
127
+ processExit.usage === undefined
128
+ ? Effect.void
129
+ : Effect.annotateCurrentSpan({
130
+ [`${TASK_TRACE_LABEL_PREFIX}.process.cpu.milliseconds`]:
131
+ Number(BigInt(processExit.usage.cpuTime.total)) / MICROSECONDS_PER_MILLISECOND,
132
+ [`${TASK_TRACE_LABEL_PREFIX}.process.memory.peak.bytes`]: processExit.usage.maxRSS,
133
+ }),
134
+ ),
135
+ Effect.andThen(spawnedProcess.handle.exitCode),
136
+ ),
137
+ ),
138
+ Effect.tap((exitCode) => Effect.annotateCurrentSpan("process.exit.code", exitCode)),
139
+ Effect.withSpan("process", {
140
+ attributes: { "process.executable.name": formatProgramNames(command) },
141
+ }),
142
+ Effect.exit,
143
+ Effect.andThen(Deferred.done(spanEndedDeferred, Exit.void)),
144
+ workerState.forkInWorkerScope,
145
+ );
146
+ const handle = yield* Deferred.await(spawnedDeferred);
147
+ return ChildProcessSpawner.makeHandle({
148
+ ...handle,
149
+ exitCode: Deferred.await(spanEndedDeferred).pipe(Effect.andThen(handle.exitCode)),
150
+ });
151
+ });
@@ -0,0 +1,76 @@
1
+ import { Deferred, Effect, Ref, Semaphore } from "effect";
2
+ import type { Fiber } from "effect";
3
+ import type { RpcClient, RpcClientError } from "effect/rpc";
4
+
5
+ import type { ProcessIdentity } from "../../../shared/claim-processes-to-report.ts";
6
+ import { dieOnInvariantFailure } from "../../../shared/invariant-errors.ts";
7
+ import { TaskEvent } from "../../../shared/rpc.ts";
8
+ import type { MainThreadRpcs, StandardInputRead } from "../../../shared/rpc.ts";
9
+ import { isAwaitedTaskFailed } from "../../../shared/task-errors.ts";
10
+
11
+ const dieUnlessAnswered = dieOnInvariantFailure(
12
+ "worker protocol",
13
+ "the main thread answers each request the worker makes",
14
+ );
15
+
16
+ export const makeMainThreadAnswers = Effect.fn("makeMainThreadAnswers")(function* (
17
+ mainThread: RpcClient.FromGroup<typeof MainThreadRpcs, RpcClientError.RpcClientError>,
18
+ reportToMainThread: (event: typeof TaskEvent.Type) => Effect.Effect<void>,
19
+ forkInWorkerScope: (effect: Effect.Effect<void>) => Effect.Effect<Fiber.Fiber<void>>,
20
+ ) {
21
+ const unclaimedStandardInputRead = yield* Ref.make<Deferred.Deferred<
22
+ typeof StandardInputRead.Type
23
+ > | null>(null);
24
+ const oneStandardInputReadAtATime = Semaphore.makeUnsafe(1);
25
+ const sendOrReuseStandardInputRequest = (taskName: string) =>
26
+ Effect.gen(function* () {
27
+ const unclaimed = yield* Ref.get(unclaimedStandardInputRead);
28
+ if (unclaimed !== null) {
29
+ return unclaimed;
30
+ }
31
+ const readDeferred = Deferred.makeUnsafe<typeof StandardInputRead.Type>();
32
+ yield* Ref.set(unclaimedStandardInputRead, readDeferred);
33
+ yield* forkInWorkerScope(
34
+ mainThread.ReadStandardInput({ taskName }).pipe(
35
+ dieUnlessAnswered,
36
+ Effect.flatMap((read) => Deferred.succeed(readDeferred, read)),
37
+ Effect.asVoid,
38
+ ),
39
+ );
40
+ return readDeferred;
41
+ });
42
+ return {
43
+ requestStandardInputRead: (taskName: string) =>
44
+ Effect.uninterruptibleMask((restore) =>
45
+ sendOrReuseStandardInputRequest(taskName).pipe(
46
+ Effect.flatMap((readDeferred) => restore(Deferred.await(readDeferred))),
47
+ Effect.tap(() => Ref.set(unclaimedStandardInputRead, null)),
48
+ ),
49
+ ).pipe(oneStandardInputReadAtATime.withPermits(1)),
50
+ requestDependencyTaskResult: (dependencyTaskName: string, requestingTaskName: string) =>
51
+ mainThread.RequestTask({ taskName: requestingTaskName, dependencyTaskName }).pipe(
52
+ Effect.catchTag("RpcClientError", (error) => dieUnlessAnswered(Effect.fail(error))),
53
+ Effect.map((answered) => answered.value),
54
+ Effect.tapError((failure) =>
55
+ isAwaitedTaskFailed(failure)
56
+ ? reportToMainThread(
57
+ TaskEvent.cases.TaskFiberReceivedAwaitedFailure.make({
58
+ taskName: requestingTaskName,
59
+ awaitedTaskName: failure.awaitedTaskName,
60
+ }),
61
+ )
62
+ : Effect.void,
63
+ ),
64
+ ),
65
+ reportStartedProcessAndAwaitRecord: (
66
+ taskName: string,
67
+ processId: number,
68
+ startTime: string | null,
69
+ ) =>
70
+ mainThread.RecordStartedProcess({ taskName, processId, startTime }).pipe(dieUnlessAnswered),
71
+ claimProcessesToReport: (
72
+ taskName: string,
73
+ processes: readonly (typeof ProcessIdentity.Type)[],
74
+ ) => mainThread.ClaimProcessReports({ taskName, processes }).pipe(dieUnlessAnswered),
75
+ };
76
+ });
@@ -0,0 +1,114 @@
1
+ import { Array as Arrays, Effect, Formatter, Option, Redacted, Schema } from "effect";
2
+
3
+ import {
4
+ holdsRedactedTaskResultKey,
5
+ REDACTED_TASK_RESULT_KEY,
6
+ } from "../../../shared/task-results.ts";
7
+ import { TaskResultNotJson } from "../../errors/index.ts";
8
+ import { describeIssueInput, formatPropertyAccess, REPORT_INPUT } from "./describe-issue-input.ts";
9
+ import { PlainObject } from "./plain-object.ts";
10
+
11
+ export const OptionalJson = Schema.UndefinedOr(Schema.Json);
12
+
13
+ const TaskResultPart = Schema.Union([
14
+ Schema.Null,
15
+ Schema.String,
16
+ Schema.Boolean,
17
+ Schema.Finite,
18
+ Schema.Redacted(Schema.String),
19
+ Schema.Array(Schema.Unknown),
20
+ PlainObject,
21
+ ]);
22
+
23
+ const isJsonLeaf = Schema.is(
24
+ Schema.Union([Schema.Null, Schema.String, Schema.Boolean, Schema.Finite]),
25
+ );
26
+
27
+ const isRedactedString = Schema.is(Schema.Redacted(Schema.String));
28
+
29
+ const decodeTaskResultPart = Schema.decodeUnknownEffect(TaskResultPart, REPORT_INPUT);
30
+
31
+ const encodeTaskResultParts = (
32
+ notJson: (valuePath: string, valueDescription: string) => TaskResultNotJson,
33
+ decodingPart: ReturnType<typeof decodeTaskResultPart>,
34
+ valuePath: string,
35
+ onPath: WeakSet<WeakKey>,
36
+ ): Effect.Effect<Schema.Json, TaskResultNotJson> =>
37
+ Effect.gen(function* () {
38
+ const part = yield* decodingPart;
39
+ const isLeaf = part === null || typeof part !== "object";
40
+ if (isLeaf) {
41
+ return part;
42
+ }
43
+ if (isRedactedString(part)) {
44
+ return { [REDACTED_TASK_RESULT_KEY]: Redacted.value(part) };
45
+ }
46
+ if (onPath.has(part)) {
47
+ return yield* notJson(valuePath, "a reference back to an enclosing value");
48
+ }
49
+ if (Object.hasOwn(part, REDACTED_TASK_RESULT_KEY)) {
50
+ return yield* notJson(
51
+ valuePath,
52
+ `an object with the reserved key ${Formatter.format(REDACTED_TASK_RESULT_KEY)}`,
53
+ );
54
+ }
55
+ onPath.add(part);
56
+ const encoded = Arrays.isArray(part)
57
+ ? yield* Effect.forEach(part, (element, index) =>
58
+ isJsonLeaf(element)
59
+ ? Effect.succeed(element)
60
+ : encodeTaskResultParts(
61
+ notJson,
62
+ decodeTaskResultPart(element),
63
+ `${valuePath}${formatPropertyAccess(index)}`,
64
+ onPath,
65
+ ),
66
+ )
67
+ : Object.fromEntries(
68
+ yield* Effect.forEach(Object.entries(part), ([key, entry]) =>
69
+ Effect.map(
70
+ isJsonLeaf(entry)
71
+ ? Effect.succeed(entry)
72
+ : encodeTaskResultParts(
73
+ notJson,
74
+ decodeTaskResultPart(entry),
75
+ `${valuePath}${formatPropertyAccess(key)}`,
76
+ onPath,
77
+ ),
78
+ (encodedEntry) => [key, encodedEntry] as const,
79
+ ),
80
+ ),
81
+ );
82
+ onPath.delete(part);
83
+ return encoded;
84
+ }).pipe(
85
+ Effect.catchTag("SchemaError", (cause) =>
86
+ Effect.fail(notJson(valuePath, describeIssueInput(cause.issue))),
87
+ ),
88
+ );
89
+
90
+ export const decodeTaskResultJson = (
91
+ taskName: string,
92
+ handedOver: TaskResultNotJson["handedOver"],
93
+ value: unknown,
94
+ ) =>
95
+ Effect.gen(function* () {
96
+ const json = yield* Schema.decodeUnknownEffect(OptionalJson)(value).pipe(
97
+ Effect.asSome,
98
+ Effect.catchTag("SchemaError", () => Effect.succeedNone),
99
+ );
100
+ const jsonWithoutReservedKey = Option.filter(json, (candidate) => {
101
+ const isBranch = candidate !== null && typeof candidate === "object";
102
+ return !isBranch || !holdsRedactedTaskResultKey(candidate);
103
+ });
104
+ if (Option.isSome(jsonWithoutReservedKey)) {
105
+ return jsonWithoutReservedKey.value;
106
+ }
107
+ return yield* encodeTaskResultParts(
108
+ (valuePath, valueDescription) =>
109
+ new TaskResultNotJson({ taskName, handedOver, valuePath, valueDescription }),
110
+ Schema.decodeUnknownEffect(TaskResultPart, REPORT_INPUT)(value),
111
+ "",
112
+ new WeakSet(),
113
+ );
114
+ });
@@ -0,0 +1,11 @@
1
+ import { Option } from "effect";
2
+ import type { Cause } from "effect";
3
+
4
+ import type { startedProcessGroup } from "../../kill-leftover-processes/index.ts";
5
+
6
+ export const makeTaskBodyRecord = () => ({
7
+ logs: new Array<string>(),
8
+ startedProcessGroups: new Map<number, ReturnType<typeof startedProcessGroup>>(),
9
+ startedAtMilliseconds: 0,
10
+ causeReportedBeforeCleanup: Option.none<Cause.Cause<unknown>>(),
11
+ });
@@ -0,0 +1,52 @@
1
+ import { Formatter, Predicate, SchemaIssue } from "effect";
2
+
3
+ export const REPORT_INPUT = { reportInput: true } as const;
4
+
5
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/u;
6
+
7
+ export const formatPropertyAccess = (key: PropertyKey) => {
8
+ if (Predicate.isNumber(key)) {
9
+ return `[${key}]`;
10
+ }
11
+ const isIdentifierKey = Predicate.isString(key) && IDENTIFIER.test(key);
12
+ if (isIdentifierKey) {
13
+ return `.${key}`;
14
+ }
15
+ return `[${Formatter.format(key)}]`;
16
+ };
17
+
18
+ const withArticle = (typeName: string) => `${/^[aeiou]/iu.test(typeName) ? "an" : "a"} ${typeName}`;
19
+
20
+ export const describeIssueInput = (issue: SchemaIssue.Issue) => {
21
+ if (!SchemaIssue.hasInput(issue)) {
22
+ return "missing";
23
+ }
24
+ const input = issue.input;
25
+ const isPrintedAsIs = input === undefined || input === null || Predicate.isNumber(input);
26
+ if (isPrintedAsIs) {
27
+ return String(input);
28
+ }
29
+ if (Predicate.isString(input)) {
30
+ return `the string ${Formatter.format(input)}`;
31
+ }
32
+ if (typeof input !== "object") {
33
+ return withArticle(typeof input);
34
+ }
35
+ if (Array.isArray(input)) {
36
+ return "an array";
37
+ }
38
+ const prototype: unknown = Object.getPrototypeOf(input);
39
+ const isPlainObject = prototype === Object.prototype || prototype === null;
40
+ if (isPlainObject) {
41
+ return "an object";
42
+ }
43
+ const hasNamedConstructor =
44
+ Predicate.hasProperty(prototype, "constructor") &&
45
+ Predicate.hasProperty(prototype.constructor, "name");
46
+ const constructorName = hasNamedConstructor ? prototype.constructor.name : undefined;
47
+ const isInstanceOfANamedClass =
48
+ Predicate.isString(constructorName) && constructorName !== "" && constructorName !== "Object";
49
+ return isInstanceOfANamedClass
50
+ ? `an instance of ${constructorName}`
51
+ : "an object whose prototype isn't Object.prototype";
52
+ };
@@ -0,0 +1,8 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const PlainObject = Schema.ObjectKeyword.check(
4
+ Schema.makeFilter((candidate) => {
5
+ const prototype: unknown = Object.getPrototypeOf(candidate);
6
+ return prototype === Object.prototype || prototype === null;
7
+ }),
8
+ );
@@ -0,0 +1,160 @@
1
+ import { workerData } from "node:worker_threads";
2
+
3
+ import type * as BunServices from "@effect/platform-bun/BunServices";
4
+ import {
5
+ Context,
6
+ Deferred,
7
+ Effect,
8
+ Layer,
9
+ Schema,
10
+ Fiber,
11
+ FiberMap,
12
+ FiberSet,
13
+ Option,
14
+ } from "effect";
15
+ import type { HttpClient } from "effect/http";
16
+ import { RpcClient, RpcSerialization, RpcWorker } from "effect/rpc";
17
+ import type { RpcMessage } from "effect/rpc";
18
+
19
+ import { completeOnceSent } from "../../../shared/complete-discarded-calls-once-sent.ts";
20
+ import { setProjectDirectoryPath } from "../../../shared/find-user-locations-in-stack-traces.ts";
21
+ import { dieOnInvariantFailure } from "../../../shared/invariant-errors.ts";
22
+ import { standardErrorLinesToTheMainThreadDeferred } from "../../../shared/print-to-standard-error.ts";
23
+ import {
24
+ MainThreadRpcs,
25
+ MessageFromMainThreadRpcServer,
26
+ TaskEvent,
27
+ WorkerData,
28
+ WorkerInitialMessage,
29
+ } from "../../../shared/rpc.ts";
30
+ import { TASK_PACKAGE } from "../../../shared/task-package.ts";
31
+ import { makeMainThreadAnswers } from "./await-main-thread-answers.ts";
32
+
33
+ const decodeMessageFromMainThreadRpcServer = Schema.decodeUnknownEffect(
34
+ MessageFromMainThreadRpcServer,
35
+ );
36
+
37
+ const makeMainThreadRpcProtocol = (mainThreadPort: MessagePort) =>
38
+ RpcClient.Protocol.make((writeResponse, clientIds) =>
39
+ Effect.gen(function* () {
40
+ const runInWorkerScope = yield* FiberSet.makeRuntime<never, void, never>();
41
+ const onMessage = (event: { readonly data: unknown }) => {
42
+ runInWorkerScope(
43
+ decodeMessageFromMainThreadRpcServer(event.data).pipe(
44
+ dieOnInvariantFailure("worker protocol", "main-thread answers match the RPC protocol"),
45
+ Effect.flatMap((response) =>
46
+ Effect.forEach(clientIds, (clientId) => writeResponse(clientId, response), {
47
+ discard: true,
48
+ }),
49
+ ),
50
+ ),
51
+ );
52
+ };
53
+ yield* Effect.acquireRelease(
54
+ Effect.sync(() => {
55
+ mainThreadPort.addEventListener("message", onMessage);
56
+ mainThreadPort.start();
57
+ }),
58
+ () =>
59
+ Effect.sync(() => {
60
+ mainThreadPort.removeEventListener("message", onMessage);
61
+ mainThreadPort.close();
62
+ }),
63
+ );
64
+ return {
65
+ send: (_clientId: number, request: RpcMessage.FromClientEncoded) =>
66
+ Effect.sync(() => {
67
+ mainThreadPort.postMessage(request);
68
+ }),
69
+ supportsAck: false,
70
+ supportsTransferables: false,
71
+ codecFor: RpcSerialization.json.codecFor,
72
+ };
73
+ }),
74
+ );
75
+
76
+ const makeTaskBodyFiberTracker = (
77
+ reportToMainThread: (event: typeof TaskEvent.Type) => Effect.Effect<void>,
78
+ ) =>
79
+ Effect.gen(function* () {
80
+ const taskBodyFibersByTaskName = yield* FiberMap.make<string>();
81
+ const taskNamesAskedToStop = new Set<string>();
82
+ const lifetimeScope = yield* Effect.scope;
83
+ return {
84
+ startTaskBodyFiber: <Success, Failure, Services>(
85
+ taskName: string,
86
+ effect: Effect.Effect<Success, Failure, Services>,
87
+ ) => FiberMap.run(taskBodyFibersByTaskName, taskName, effect).pipe(Effect.asVoid),
88
+ requestTaskBodyInterruption: (taskName: string) =>
89
+ Effect.gen(function* () {
90
+ taskNamesAskedToStop.add(taskName);
91
+ yield* reportToMainThread(TaskEvent.cases.TaskFiberStartedCleanUp.make({ taskName }));
92
+ const bodyFiber = FiberMap.getUnsafe(taskBodyFibersByTaskName, taskName);
93
+ if (Option.isSome(bodyFiber)) {
94
+ yield* Effect.forkIn(Fiber.interrupt(bodyFiber.value), lifetimeScope);
95
+ }
96
+ }),
97
+ wasAskedToStop: (taskName: string) => Effect.sync(() => taskNamesAskedToStop.has(taskName)),
98
+ forkInWorkerScope: <Success, Failure, Services>(
99
+ effect: Effect.Effect<Success, Failure, Services>,
100
+ ) => Effect.forkIn(effect, lifetimeScope),
101
+ };
102
+ });
103
+
104
+ const connectToMainThread = Effect.gen(function* () {
105
+ const decodedWorkerData = yield* Schema.decodeUnknownEffect(WorkerData)(workerData).pipe(
106
+ dieOnInvariantFailure("worker startup", "worker data matches the worker protocol"),
107
+ );
108
+ yield* setProjectDirectoryPath(decodedWorkerData.projectDirectoryPath);
109
+ const { mainThreadPort } = yield* RpcWorker.initialMessage(WorkerInitialMessage).pipe(
110
+ dieOnInvariantFailure("worker startup", "the main thread hands over its RPC port first"),
111
+ );
112
+ const mainThread = yield* RpcClient.make(MainThreadRpcs, { disableTracing: true }).pipe(
113
+ Effect.provideServiceEffect(RpcClient.Protocol, makeMainThreadRpcProtocol(mainThreadPort)),
114
+ );
115
+ const reportScope = yield* Effect.scope;
116
+ const reportToMainThread = (event: typeof TaskEvent.Type) =>
117
+ mainThread
118
+ .ReportTaskEvent(event, { discard: true })
119
+ .pipe(
120
+ dieOnInvariantFailure("worker protocol", "reports reach the main thread"),
121
+ completeOnceSent(reportScope),
122
+ );
123
+ const runInWorkerScope = yield* FiberSet.makeRuntime<never, void, never>();
124
+ const reportToMainThreadNow = (event: typeof TaskEvent.Type) => {
125
+ runInWorkerScope(reportToMainThread(event));
126
+ };
127
+ yield* Deferred.succeed(standardErrorLinesToTheMainThreadDeferred, (logLevel, text) =>
128
+ reportToMainThread(TaskEvent.cases.StandardErrorLineRequested.make({ logLevel, text })),
129
+ );
130
+ return { ...decodedWorkerData, mainThread, reportToMainThread, reportToMainThreadNow };
131
+ });
132
+
133
+ const makeWorkerState = Effect.gen(function* () {
134
+ const mainThreadConnection = yield* connectToMainThread;
135
+ const taskBodyFibers = yield* makeTaskBodyFiberTracker(mainThreadConnection.reportToMainThread);
136
+ const runtimeServices = yield* Effect.context<BunServices.BunServices | HttpClient.HttpClient>();
137
+ const mainThreadAnswers = yield* makeMainThreadAnswers(
138
+ mainThreadConnection.mainThread,
139
+ mainThreadConnection.reportToMainThread,
140
+ taskBodyFibers.forkInWorkerScope,
141
+ );
142
+ return {
143
+ ...mainThreadConnection,
144
+ provideRuntimeServices: <Success, Failure, Services>(
145
+ effect: Effect.Effect<Success, Failure, Services>,
146
+ ) => Effect.provide(effect, runtimeServices),
147
+ ...taskBodyFibers,
148
+ ...mainThreadAnswers,
149
+ };
150
+ });
151
+
152
+ export class WorkerState extends Context.Service<
153
+ WorkerState,
154
+ Effect.Success<typeof makeWorkerState>
155
+ >()(`${TASK_PACKAGE.name}/WorkerState`) {}
156
+
157
+ export const WorkerStateLive = Layer.effect(WorkerState, makeWorkerState);
158
+
159
+ export const reportToMainThread = (event: typeof TaskEvent.Type) =>
160
+ WorkerState.use((workerState) => workerState.reportToMainThread(event));
@@ -0,0 +1,41 @@
1
+ import { Schema } from "effect";
2
+
3
+ import { TASK_ERROR_FIELDS } from "../../../shared/error-fields.ts";
4
+ import { describeCauseMessage } from "../../../shared/report-errors.ts";
5
+
6
+ export class ProcessGroupNotSignaledByWorker extends Schema.TaggedError<ProcessGroupNotSignaledByWorker>()(
7
+ "ProcessGroupNotSignaledByWorker",
8
+ {
9
+ ...TASK_ERROR_FIELDS,
10
+ processGroupId: Schema.Int,
11
+ programName: Schema.String,
12
+ cause: Schema.Defect(),
13
+ },
14
+ ) {
15
+ public override get message() {
16
+ return `worker could not signal process group ${this.processGroupId} of ${this.programName} because ${describeCauseMessage(this.cause)}`;
17
+ }
18
+ }
19
+
20
+ export class TaskNotRegisteredInWorker extends Schema.TaggedError<TaskNotRegisteredInWorker>()(
21
+ "TaskNotRegisteredInWorker",
22
+ { ...TASK_ERROR_FIELDS, taskName: Schema.String, filePath: Schema.String },
23
+ ) {
24
+ public override get message() {
25
+ return `importing this file in a worker made no task named "${this.taskName}"`;
26
+ }
27
+ }
28
+
29
+ export class TaskRegisteredAgainInWorker extends Schema.TaggedError<TaskRegisteredAgainInWorker>()(
30
+ "TaskRegisteredAgainInWorker",
31
+ {
32
+ ...TASK_ERROR_FIELDS,
33
+ taskName: Schema.String,
34
+ filePath: Schema.String,
35
+ madeCount: Schema.Int,
36
+ },
37
+ ) {
38
+ public override get message() {
39
+ return `importing this file in a worker made ${this.madeCount} tasks named "${this.taskName}"`;
40
+ }
41
+ }
@@ -0,0 +1,25 @@
1
+ import { Array as Arrays } from "effect";
2
+
3
+ import type { PrintableError } from "../lib/errors/index.ts";
4
+ import { UNTAGGED_FAILURE_TAG, withLocationOrElse } from "./report-errors.ts";
5
+
6
+ export const attributeFailures = (
7
+ carrier: {
8
+ readonly _tag: string;
9
+ readonly taskName: string;
10
+ readonly failures: Arrays.NonEmptyReadonlyArray<PrintableError>;
11
+ },
12
+ describeUntaggedFailure: (failureMessage: string) => string,
13
+ ): Arrays.NonEmptyReadonlyArray<PrintableError> =>
14
+ Arrays.map(carrier.failures, (failure) =>
15
+ failure.tag === UNTAGGED_FAILURE_TAG
16
+ ? {
17
+ tag: carrier._tag,
18
+ location: failure.location ?? carrier.taskName,
19
+ message: describeUntaggedFailure(failure.message),
20
+ }
21
+ : withLocationOrElse(failure, carrier.taskName),
22
+ );
23
+
24
+ export const joinMessages = (reportedErrors: readonly PrintableError[]) =>
25
+ reportedErrors.map((reportedError) => reportedError.message).join("\n");
@@ -0,0 +1,22 @@
1
+ import { Effect, Schema } from "effect";
2
+
3
+ export const ProcessIdentity = Schema.Struct({ processId: Schema.Int, startTime: Schema.String });
4
+
5
+ export const processIdentityKey = (identity: typeof ProcessIdentity.Type) =>
6
+ `${identity.processId}@${identity.startTime}`;
7
+
8
+ export const makeProcessReportClaimer = () => {
9
+ const reportedProcessKeys = new Set<string>();
10
+ const claimProcessesToReport = (processes: readonly (typeof ProcessIdentity.Type)[]) =>
11
+ Effect.sync(() =>
12
+ processes.filter((identity) => {
13
+ const key = processIdentityKey(identity);
14
+ if (reportedProcessKeys.has(key)) {
15
+ return false;
16
+ }
17
+ reportedProcessKeys.add(key);
18
+ return true;
19
+ }),
20
+ );
21
+ return { claimProcessesToReport };
22
+ };