@playfast/muxmaxing-config 0.1.2 → 0.2.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.
- package/package.json +16 -16
- package/src/index.ts +1 -2
- package/src/protocol.ts +11 -8
- package/src/worker-entry.ts +107 -84
package/package.json
CHANGED
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playfast/muxmaxing-config",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.2.0",
|
|
5
4
|
"description": "Author API for muxmaxing.config.ts — a repo-committed hook bridge between the host device and muxmaxing work units: typed host-input collection plus Effect-native lifecycle hooks (machine created, new terminal, removed) with bash bridged into the unit.",
|
|
6
5
|
"keywords": [
|
|
7
|
-
"
|
|
6
|
+
"devcontainer",
|
|
8
7
|
"effect",
|
|
9
8
|
"hooks",
|
|
10
|
-
"
|
|
9
|
+
"muxmaxing",
|
|
11
10
|
"provisioning"
|
|
12
11
|
],
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/playfast/reform/issues"
|
|
14
|
+
},
|
|
13
15
|
"license": "MIT",
|
|
14
16
|
"repository": {
|
|
15
17
|
"type": "git",
|
|
16
18
|
"url": "https://github.com/playfast/reform.git",
|
|
17
19
|
"directory": "packages/muxmaxing-config"
|
|
18
20
|
},
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
22
26
|
"sideEffects": false,
|
|
23
27
|
"exports": {
|
|
24
28
|
"./package.json": "./package.json",
|
|
25
29
|
".": "./src/index.ts",
|
|
26
30
|
"./*": "./src/*.ts"
|
|
27
31
|
},
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
32
35
|
"scripts": {
|
|
33
36
|
"clean": "rm -rf dist .tsbuildinfo",
|
|
34
37
|
"check": "tsc --noEmit",
|
|
@@ -36,13 +39,10 @@
|
|
|
36
39
|
"lint": "oxlint src",
|
|
37
40
|
"lint:fix": "oxlint --fix src"
|
|
38
41
|
},
|
|
39
|
-
"peerDependencies": {
|
|
40
|
-
"effect": "*"
|
|
41
|
-
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/bun": "^1.3.14"
|
|
44
44
|
},
|
|
45
|
-
"
|
|
46
|
-
"
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"effect": "*"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -66,8 +66,7 @@ export interface MuxmaxingConfigExternalApi<A, I> {
|
|
|
66
66
|
|
|
67
67
|
export type MuxmaxingConfig<A, I> = MuxmaxingConfigExternalApi<A, I>
|
|
68
68
|
|
|
69
|
-
export const defineConfig = <A, I>(config: MuxmaxingConfig<A, I>): MuxmaxingConfig<A, I> =>
|
|
70
|
-
config
|
|
69
|
+
export const defineConfig = <A, I>(config: MuxmaxingConfig<A, I>): MuxmaxingConfig<A, I> => config
|
|
71
70
|
|
|
72
71
|
export const isMuxmaxingConfig = (
|
|
73
72
|
candidate: unknown,
|
package/src/protocol.ts
CHANGED
|
@@ -31,8 +31,10 @@ export const BashOk: S.TaggedStruct<
|
|
|
31
31
|
> = S.TaggedStruct('BashOk', { stdout: S.String, stderr: S.String, exitCode: S.Number })
|
|
32
32
|
export type BashOk = typeof BashOk.Type
|
|
33
33
|
|
|
34
|
-
export const BashFailed: S.TaggedStruct<'BashFailed', { reason: typeof S.String }> =
|
|
35
|
-
|
|
34
|
+
export const BashFailed: S.TaggedStruct<'BashFailed', { reason: typeof S.String }> = S.TaggedStruct(
|
|
35
|
+
'BashFailed',
|
|
36
|
+
{ reason: S.String },
|
|
37
|
+
)
|
|
36
38
|
export type BashFailed = typeof BashFailed.Type
|
|
37
39
|
|
|
38
40
|
export const BashReply: S.TaggedStruct<
|
|
@@ -61,18 +63,19 @@ export const BashRequest: S.TaggedStruct<
|
|
|
61
63
|
})
|
|
62
64
|
export type BashRequest = typeof BashRequest.Type
|
|
63
65
|
|
|
64
|
-
export const LogMessage: S.TaggedStruct<'Log', { line: typeof S.String }> = S.TaggedStruct(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
)
|
|
66
|
+
export const LogMessage: S.TaggedStruct<'Log', { line: typeof S.String }> = S.TaggedStruct('Log', {
|
|
67
|
+
line: S.String,
|
|
68
|
+
})
|
|
68
69
|
export type LogMessage = typeof LogMessage.Type
|
|
69
70
|
|
|
70
71
|
export const HookDone: S.TaggedStruct<'HookDone', { hostInput: S.NullOr<typeof S.Unknown> }> =
|
|
71
72
|
S.TaggedStruct('HookDone', { hostInput: S.NullOr(S.Unknown) })
|
|
72
73
|
export type HookDone = typeof HookDone.Type
|
|
73
74
|
|
|
74
|
-
export const HookFailed: S.TaggedStruct<'HookFailed', { reason: typeof S.String }> =
|
|
75
|
-
|
|
75
|
+
export const HookFailed: S.TaggedStruct<'HookFailed', { reason: typeof S.String }> = S.TaggedStruct(
|
|
76
|
+
'HookFailed',
|
|
77
|
+
{ reason: S.String },
|
|
78
|
+
)
|
|
76
79
|
export type HookFailed = typeof HookFailed.Type
|
|
77
80
|
|
|
78
81
|
export const WorkerToEngine: S.Union<
|
package/src/worker-entry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Cause, Data, Effect, Either, Match, Option, Ref, Schema as S } from 'effect'
|
|
2
|
+
import { parentPort } from 'node:worker_threads'
|
|
2
3
|
import {
|
|
3
4
|
type BashReply,
|
|
4
5
|
BashRequest,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
LogMessage,
|
|
9
10
|
type RunHookRequest,
|
|
10
11
|
WorkerToEngine,
|
|
11
|
-
} from '
|
|
12
|
+
} from '@playfast/muxmaxing-config/protocol'
|
|
12
13
|
import {
|
|
13
14
|
type BashResult,
|
|
14
15
|
HookBashError,
|
|
@@ -16,10 +17,12 @@ import {
|
|
|
16
17
|
isMuxmaxingConfig,
|
|
17
18
|
type MuxmaxingConfig,
|
|
18
19
|
type UnitInfo,
|
|
19
|
-
} from '
|
|
20
|
+
} from '@playfast/muxmaxing-config'
|
|
20
21
|
|
|
21
22
|
declare const self: Worker
|
|
22
23
|
|
|
24
|
+
const hasWebWorkerScope = typeof self !== 'undefined'
|
|
25
|
+
|
|
23
26
|
class WorkerHookError extends Data.TaggedError('WorkerHookError')<{
|
|
24
27
|
readonly reason: string
|
|
25
28
|
}> {
|
|
@@ -30,7 +33,16 @@ class WorkerHookError extends Data.TaggedError('WorkerHookError')<{
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const send = (message: WorkerToEngine): void => {
|
|
33
|
-
|
|
36
|
+
const encoded = S.encodeSync(WorkerToEngine)(message)
|
|
37
|
+
if (hasWebWorkerScope) {
|
|
38
|
+
self.postMessage(encoded)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (parentPort !== null) {
|
|
42
|
+
parentPort.postMessage(encoded)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
Effect.runSync(Effect.dieMessage('worker entry has no parent message port'))
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
const describe = (cause: unknown): string =>
|
|
@@ -54,8 +66,7 @@ const makeBridgedBash =
|
|
|
54
66
|
stderr: ok.stderr,
|
|
55
67
|
exitCode: ok.exitCode,
|
|
56
68
|
}),
|
|
57
|
-
BashFailed: (failed) =>
|
|
58
|
-
Effect.fail(new HookBashError({ reason: failed.reason })),
|
|
69
|
+
BashFailed: (failed) => Effect.fail(new HookBashError({ reason: failed.reason })),
|
|
59
70
|
}),
|
|
60
71
|
Match.exhaustive,
|
|
61
72
|
),
|
|
@@ -74,18 +85,14 @@ const loadConfig = (
|
|
|
74
85
|
): Effect.Effect<MuxmaxingConfig<unknown, unknown>, WorkerHookError> =>
|
|
75
86
|
Effect.tryPromise({
|
|
76
87
|
try: (): Promise<unknown> => import(configPath),
|
|
77
|
-
catch: (cause) =>
|
|
78
|
-
new WorkerHookError({ reason: `config import failed: ${describe(cause)}` }),
|
|
88
|
+
catch: (cause) => new WorkerHookError({ reason: `config import failed: ${describe(cause)}` }),
|
|
79
89
|
}).pipe(
|
|
80
90
|
Effect.flatMap((moduleExports) => {
|
|
81
91
|
const record: Record<string, unknown> =
|
|
82
|
-
typeof moduleExports === 'object' && moduleExports !== null
|
|
83
|
-
? { ...moduleExports }
|
|
84
|
-
: {}
|
|
92
|
+
typeof moduleExports === 'object' && moduleExports !== null ? { ...moduleExports } : {}
|
|
85
93
|
const defaulted = Option.fromNullable(record['default'])
|
|
86
94
|
return Option.match(defaulted, {
|
|
87
|
-
onNone: () =>
|
|
88
|
-
Effect.fail(new WorkerHookError({ reason: 'config has no default export' })),
|
|
95
|
+
onNone: () => Effect.fail(new WorkerHookError({ reason: 'config has no default export' })),
|
|
89
96
|
onSome: (config) => {
|
|
90
97
|
if (isMuxmaxingConfig(config)) {
|
|
91
98
|
return Effect.succeed(config)
|
|
@@ -116,80 +123,80 @@ const hookFailure =
|
|
|
116
123
|
(cause: unknown): WorkerHookError =>
|
|
117
124
|
new WorkerHookError({ reason: `${hookName} failed: ${describe(cause)}` })
|
|
118
125
|
|
|
119
|
-
const runRequested = Effect.fn('runRequested')(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
126
|
+
const runRequested = Effect.fn('runRequested')(function* (
|
|
127
|
+
request: RunHookRequest,
|
|
128
|
+
): Effect.fn.Return<HookDone, WorkerHookError> {
|
|
129
|
+
const config = yield* loadConfig(request.configPath)
|
|
130
|
+
const idRef = yield* Ref.make(0)
|
|
131
|
+
const unit: UnitInfo = {
|
|
132
|
+
id: request.unit.id,
|
|
133
|
+
workspaceFolder: request.unit.workspaceFolder,
|
|
134
|
+
}
|
|
135
|
+
const workerEnv = typeof Bun === 'undefined' ? process.env : Bun.env
|
|
136
|
+
const host: HostBridge = { bash: makeBridgedBash(idRef, 'host'), env: workerEnv }
|
|
137
|
+
const unitBridge = { ...unit, bash: makeBridgedBash(idRef, 'unit') }
|
|
138
|
+
|
|
139
|
+
// Undefined consuming hooks are intentional silent no-ops.
|
|
140
|
+
const runOptional = <Ctx>(
|
|
141
|
+
hookName: string,
|
|
142
|
+
hook: ((ctx: Ctx) => Effect.Effect<void, unknown>) | undefined,
|
|
143
|
+
makeCtx: (hostInput: unknown) => Ctx,
|
|
144
|
+
): Effect.Effect<HookDone, WorkerHookError> =>
|
|
145
|
+
Option.match(Option.fromNullable(hook), {
|
|
146
|
+
onNone: () => Effect.succeed(HookDone.make({ hostInput: null })),
|
|
147
|
+
onSome: (run) =>
|
|
148
|
+
decodeHostInput(config, request.hostInput).pipe(
|
|
149
|
+
Effect.flatMap((hostInput) =>
|
|
150
|
+
run(makeCtx(hostInput)).pipe(Effect.mapError(hookFailure(hookName))),
|
|
144
151
|
),
|
|
145
|
-
|
|
152
|
+
Effect.map(() => HookDone.make({ hostInput: null })),
|
|
153
|
+
),
|
|
154
|
+
})
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Effect.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
(issue)
|
|
157
|
-
|
|
158
|
-
reason: `hostInput encode failed: ${String(issue)}`,
|
|
159
|
-
}),
|
|
160
|
-
),
|
|
161
|
-
),
|
|
156
|
+
return yield* Match.value(request.hook).pipe(
|
|
157
|
+
Match.when('collect', () =>
|
|
158
|
+
config.collectHostDeviceInput({ host, unit, log }).pipe(
|
|
159
|
+
Effect.mapError(hookFailure('collectHostDeviceInput')),
|
|
160
|
+
Effect.flatMap((collected) =>
|
|
161
|
+
S.encodeUnknown(config.hostInput)(collected).pipe(
|
|
162
|
+
Effect.mapError(
|
|
163
|
+
(issue) =>
|
|
164
|
+
new WorkerHookError({
|
|
165
|
+
reason: `hostInput encode failed: ${String(issue)}`,
|
|
166
|
+
}),
|
|
162
167
|
),
|
|
163
|
-
Effect.map((encoded) => HookDone.make({ hostInput: encoded ?? null })),
|
|
164
168
|
),
|
|
169
|
+
),
|
|
170
|
+
Effect.map((encoded) => HookDone.make({ hostInput: encoded ?? null })),
|
|
165
171
|
),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
),
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
),
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
172
|
+
),
|
|
173
|
+
Match.when('machineCreated', () =>
|
|
174
|
+
runOptional('onWorkUnitMachineCreated', config.onWorkUnitMachineCreated, (hostInput) => ({
|
|
175
|
+
hostInput,
|
|
176
|
+
host,
|
|
177
|
+
unit: unitBridge,
|
|
178
|
+
log,
|
|
179
|
+
})),
|
|
180
|
+
),
|
|
181
|
+
Match.when('newTerminal', () =>
|
|
182
|
+
runOptional('onNewTerminal', config.onNewTerminal, (hostInput) => ({
|
|
183
|
+
hostInput,
|
|
184
|
+
host,
|
|
185
|
+
unit: unitBridge,
|
|
186
|
+
log,
|
|
187
|
+
})),
|
|
188
|
+
),
|
|
189
|
+
Match.when('removed', () =>
|
|
190
|
+
runOptional('onWorkUnitRemoved', config.onWorkUnitRemoved, (hostInput) => ({
|
|
191
|
+
hostInput,
|
|
192
|
+
host,
|
|
193
|
+
unit,
|
|
194
|
+
log,
|
|
195
|
+
})),
|
|
196
|
+
),
|
|
197
|
+
Match.exhaustive,
|
|
198
|
+
)
|
|
199
|
+
})
|
|
193
200
|
|
|
194
201
|
const handleRunHook = (request: RunHookRequest): void => {
|
|
195
202
|
void Effect.runPromise(
|
|
@@ -201,15 +208,21 @@ const handleRunHook = (request: RunHookRequest): void => {
|
|
|
201
208
|
// Drop the listener so the event loop drains and the worker exits.
|
|
202
209
|
Effect.ensuring(
|
|
203
210
|
Effect.sync(() => {
|
|
204
|
-
|
|
211
|
+
if (hasWebWorkerScope) {
|
|
212
|
+
self.onmessage = null
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
if (parentPort !== null) {
|
|
216
|
+
parentPort.close()
|
|
217
|
+
}
|
|
205
218
|
}),
|
|
206
219
|
),
|
|
207
220
|
),
|
|
208
221
|
)
|
|
209
222
|
}
|
|
210
223
|
|
|
211
|
-
|
|
212
|
-
Either.match(S.decodeUnknownEither(EngineToWorker)(
|
|
224
|
+
const handleMessage = (payload: unknown): void => {
|
|
225
|
+
Either.match(S.decodeUnknownEither(EngineToWorker)(payload), {
|
|
213
226
|
onLeft: () => send(HookFailed.make({ reason: 'undecodable engine message' })),
|
|
214
227
|
onRight: (message) =>
|
|
215
228
|
Match.value(message).pipe(
|
|
@@ -225,3 +238,13 @@ self.onmessage = (event: MessageEvent): void => {
|
|
|
225
238
|
),
|
|
226
239
|
})
|
|
227
240
|
}
|
|
241
|
+
|
|
242
|
+
if (hasWebWorkerScope) {
|
|
243
|
+
self.onmessage = (event: MessageEvent): void => handleMessage(event.data)
|
|
244
|
+
}
|
|
245
|
+
if (!hasWebWorkerScope && parentPort !== null) {
|
|
246
|
+
parentPort.on('message', handleMessage)
|
|
247
|
+
}
|
|
248
|
+
if (!hasWebWorkerScope && parentPort === null) {
|
|
249
|
+
Effect.runSync(Effect.dieMessage('worker entry has no parent message port'))
|
|
250
|
+
}
|