@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 CHANGED
@@ -1,34 +1,37 @@
1
1
  {
2
2
  "name": "@playfast/muxmaxing-config",
3
- "version": "0.1.2",
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
- "muxmaxing",
6
+ "devcontainer",
8
7
  "effect",
9
8
  "hooks",
10
- "devcontainer",
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
- "bugs": {
20
- "url": "https://github.com/playfast/reform/issues"
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
- "files": [
29
- "src",
30
- "README.md"
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
- "publishConfig": {
46
- "access": "public"
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
- S.TaggedStruct('BashFailed', { reason: S.String })
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
- 'Log',
66
- { line: S.String },
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
- S.TaggedStruct('HookFailed', { reason: S.String })
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<
@@ -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 './protocol'
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 './index'
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
- self.postMessage(S.encodeSync(WorkerToEngine)(message))
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
- function* (request: RunHookRequest): Effect.fn.Return<HookDone, WorkerHookError> {
121
- const config = yield* loadConfig(request.configPath)
122
- const idRef = yield* Ref.make(0)
123
- const unit: UnitInfo = {
124
- id: request.unit.id,
125
- workspaceFolder: request.unit.workspaceFolder,
126
- }
127
- const host: HostBridge = { bash: makeBridgedBash(idRef, 'host'), env: Bun.env }
128
- const unitBridge = { ...unit, bash: makeBridgedBash(idRef, 'unit') }
129
-
130
- // Undefined consuming hooks are intentional silent no-ops.
131
- const runOptional = <Ctx>(
132
- hookName: string,
133
- hook: ((ctx: Ctx) => Effect.Effect<void, unknown>) | undefined,
134
- makeCtx: (hostInput: unknown) => Ctx,
135
- ): Effect.Effect<HookDone, WorkerHookError> =>
136
- Option.match(Option.fromNullable(hook), {
137
- onNone: () => Effect.succeed(HookDone.make({ hostInput: null })),
138
- onSome: (run) =>
139
- decodeHostInput(config, request.hostInput).pipe(
140
- Effect.flatMap((hostInput) =>
141
- run(makeCtx(hostInput)).pipe(Effect.mapError(hookFailure(hookName))),
142
- ),
143
- Effect.map(() => HookDone.make({ hostInput: null })),
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
- return yield* Match.value(request.hook).pipe(
148
- Match.when('collect', () =>
149
- config
150
- .collectHostDeviceInput({ host, unit, log })
151
- .pipe(
152
- Effect.mapError(hookFailure('collectHostDeviceInput')),
153
- Effect.flatMap((collected) =>
154
- S.encodeUnknown(config.hostInput)(collected).pipe(
155
- Effect.mapError(
156
- (issue) =>
157
- new WorkerHookError({
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
- Match.when('machineCreated', () =>
167
- runOptional('onWorkUnitMachineCreated', config.onWorkUnitMachineCreated, (hostInput) => ({
168
- hostInput,
169
- host,
170
- unit: unitBridge,
171
- log,
172
- })),
173
- ),
174
- Match.when('newTerminal', () =>
175
- runOptional('onNewTerminal', config.onNewTerminal, (hostInput) => ({
176
- hostInput,
177
- host,
178
- unit: unitBridge,
179
- log,
180
- })),
181
- ),
182
- Match.when('removed', () =>
183
- runOptional('onWorkUnitRemoved', config.onWorkUnitRemoved, (hostInput) => ({
184
- hostInput,
185
- host,
186
- unit,
187
- log,
188
- })),
189
- ),
190
- Match.exhaustive,
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
- self.onmessage = null
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
- self.onmessage = (event: MessageEvent): void => {
212
- Either.match(S.decodeUnknownEither(EngineToWorker)(event.data), {
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
+ }