devsess 0.3.4 → 0.5.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/LICENSE +201 -0
- package/README.md +3 -3
- package/dist/async/index.d.ts +1 -1
- package/dist/async/index.d.ts.map +1 -1
- package/dist/async/index.js +9 -7
- package/dist/async/index.js.map +1 -1
- package/dist/async/platform.d.ts +2 -2
- package/dist/async/platform.d.ts.map +1 -1
- package/dist/async/run-dev-cli.d.ts +1 -1
- package/dist/async/run-dev-cli.d.ts.map +1 -1
- package/dist/chunk-KG24ZAKO.js +166 -0
- package/dist/chunk-KG24ZAKO.js.map +1 -0
- package/dist/{chunk-ULIVG5ID.js → chunk-MBPIOYW3.js} +166 -233
- package/dist/chunk-MBPIOYW3.js.map +1 -0
- package/dist/chunk-OODLZZCC.js +287 -0
- package/dist/chunk-OODLZZCC.js.map +1 -0
- package/dist/chunk-UOIMVHGX.js +29 -0
- package/dist/chunk-UOIMVHGX.js.map +1 -0
- package/dist/current-session.d.ts +1 -0
- package/dist/current-session.d.ts.map +1 -1
- package/dist/dev/daemon-client.d.ts +21 -0
- package/dist/dev/daemon-client.d.ts.map +1 -0
- package/dist/dev/daemon-readiness.d.ts.map +1 -1
- package/dist/dev/daemon-services.d.ts +3 -0
- package/dist/dev/daemon-services.d.ts.map +1 -0
- package/dist/dev/sticky-port.d.ts +4 -1
- package/dist/dev/sticky-port.d.ts.map +1 -1
- package/dist/dev/subprocess.d.ts +1 -1
- package/dist/dev/subprocess.d.ts.map +1 -1
- package/dist/dev-sessions.d.ts +2 -0
- package/dist/dev-sessions.d.ts.map +1 -1
- package/dist/index.js +10 -26
- package/dist/index.js.map +1 -1
- package/dist/lock.d.ts +3 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/pglite/index.d.ts +2 -2
- package/dist/pglite/index.d.ts.map +1 -1
- package/dist/pglite/index.js +20 -30
- package/dist/pglite/index.js.map +1 -1
- package/dist/services/container.d.ts +17 -0
- package/dist/services/container.d.ts.map +1 -0
- package/dist/services/core.d.ts +30 -0
- package/dist/services/core.d.ts.map +1 -0
- package/dist/services/docker-args.d.ts +31 -0
- package/dist/services/docker-args.d.ts.map +1 -0
- package/dist/services/index.d.ts +50 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +379 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/local.d.ts +11 -0
- package/dist/services/local.d.ts.map +1 -0
- package/dist/services/shared/callsite.d.ts +4 -0
- package/dist/services/shared/callsite.d.ts.map +1 -0
- package/dist/services/shared/entry.d.ts +2 -0
- package/dist/services/shared/entry.d.ts.map +1 -0
- package/dist/services/shared/entry.js +137 -0
- package/dist/services/shared/entry.js.map +1 -0
- package/dist/services/shared/host.d.ts +5 -0
- package/dist/services/shared/host.d.ts.map +1 -0
- package/dist/services/shared/index.d.ts +11 -0
- package/dist/services/shared/index.d.ts.map +1 -0
- package/dist/services/shared/protocol.d.ts +39 -0
- package/dist/services/shared/protocol.d.ts.map +1 -0
- package/dist/services/shared/root.d.ts +5 -0
- package/dist/services/shared/root.d.ts.map +1 -0
- package/dist/socket-line.d.ts +3 -0
- package/dist/socket-line.d.ts.map +1 -0
- package/package.json +94 -88
- package/dist/chunk-ULIVG5ID.js.map +0 -1
|
@@ -7,6 +7,7 @@ var DevSession;
|
|
|
7
7
|
((DevSession2) => {
|
|
8
8
|
function make(input) {
|
|
9
9
|
return {
|
|
10
|
+
rootDir: input.rootDir,
|
|
10
11
|
name: input.name,
|
|
11
12
|
lastModifiedAt: input.lastModifiedAt,
|
|
12
13
|
path: (relativePath) => Effect.succeed(input.pathJoin(relativePath)),
|
|
@@ -60,7 +61,7 @@ var findProjectRoot = Effect.gen(function* () {
|
|
|
60
61
|
});
|
|
61
62
|
return yield* walkUp(startDir);
|
|
62
63
|
});
|
|
63
|
-
var buildDevSessionsCore = (sessionsDir) => Effect.gen(function* () {
|
|
64
|
+
var buildDevSessionsCore = (sessionsDir, rootDir) => Effect.gen(function* () {
|
|
64
65
|
const fs = yield* FileSystem;
|
|
65
66
|
const path = yield* Path;
|
|
66
67
|
yield* Effect.logDebug(`DevSessions dir: ${sessionsDir}`);
|
|
@@ -75,7 +76,7 @@ var buildDevSessionsCore = (sessionsDir) => Effect.gen(function* () {
|
|
|
75
76
|
DevSession.make({
|
|
76
77
|
name: entry,
|
|
77
78
|
lastModifiedAt: Option.getOrNull(stat.mtime),
|
|
78
|
-
rootDir
|
|
79
|
+
rootDir,
|
|
79
80
|
pathJoin: (relativePath) => path.join(sessionsDir, entry, relativePath)
|
|
80
81
|
})
|
|
81
82
|
);
|
|
@@ -93,7 +94,7 @@ var buildDevSessionsCore = (sessionsDir) => Effect.gen(function* () {
|
|
|
93
94
|
return DevSession.make({
|
|
94
95
|
name: slug,
|
|
95
96
|
lastModifiedAt: null,
|
|
96
|
-
rootDir
|
|
97
|
+
rootDir,
|
|
97
98
|
pathJoin: (relativePath) => path.join(sessionsDir, slug, relativePath)
|
|
98
99
|
});
|
|
99
100
|
});
|
|
@@ -109,14 +110,25 @@ var buildDevSessionsCore = (sessionsDir) => Effect.gen(function* () {
|
|
|
109
110
|
}
|
|
110
111
|
return yield* createSession;
|
|
111
112
|
});
|
|
112
|
-
|
|
113
|
+
const getOrCreate = (name) => Effect.gen(function* () {
|
|
114
|
+
yield* fs.makeDirectory(path.join(sessionsDir, name), {
|
|
115
|
+
recursive: true
|
|
116
|
+
});
|
|
117
|
+
return DevSession.make({
|
|
118
|
+
name,
|
|
119
|
+
rootDir,
|
|
120
|
+
lastModifiedAt: null,
|
|
121
|
+
pathJoin: (relativePath) => path.join(sessionsDir, name, relativePath)
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return { getSessions, createSession, getLatestOrCreate, getOrCreate };
|
|
113
125
|
});
|
|
114
126
|
var buildDevSessionsLayer = (rootDir) => Layer.effect(
|
|
115
127
|
DevSessions,
|
|
116
128
|
Effect.gen(function* () {
|
|
117
129
|
const path = yield* Path;
|
|
118
130
|
const sessionsDir = path.join(rootDir, ".data", "sessions");
|
|
119
|
-
const core = yield* buildDevSessionsCore(sessionsDir);
|
|
131
|
+
const core = yield* buildDevSessionsCore(sessionsDir, rootDir);
|
|
120
132
|
return {
|
|
121
133
|
dir: rootDir,
|
|
122
134
|
path: (relativePath) => path.join(rootDir, relativePath),
|
|
@@ -125,185 +137,10 @@ var buildDevSessionsLayer = (rootDir) => Layer.effect(
|
|
|
125
137
|
})
|
|
126
138
|
);
|
|
127
139
|
|
|
128
|
-
// src/dev/
|
|
129
|
-
import {
|
|
130
|
-
import { createRequire } from "module";
|
|
131
|
-
import { dirname, join, resolve } from "path";
|
|
132
|
-
import { Deferred, Effect as Effect3 } from "effect";
|
|
140
|
+
// src/dev/session-state.ts
|
|
141
|
+
import { Data as Data2, Effect as Effect2 } from "effect";
|
|
133
142
|
import { FileSystem as FileSystem2 } from "effect/FileSystem";
|
|
134
143
|
import { Path as Path2 } from "effect/Path";
|
|
135
|
-
|
|
136
|
-
// src/dev/daemon-readiness.ts
|
|
137
|
-
import { createConnection } from "net";
|
|
138
|
-
import { StringDecoder } from "string_decoder";
|
|
139
|
-
import { Effect as Effect2, Exit, Schema } from "effect";
|
|
140
|
-
var RESPONSE_TIMEOUT_MS = 5e3;
|
|
141
|
-
var MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
142
|
-
var DaemonResponse = Schema.fromJsonString(
|
|
143
|
-
Schema.Struct({
|
|
144
|
-
version: Schema.Literal(1),
|
|
145
|
-
requestId: Schema.NonEmptyString,
|
|
146
|
-
ok: Schema.Boolean,
|
|
147
|
-
result: Schema.optional(Schema.Unknown),
|
|
148
|
-
error: Schema.optional(Schema.String)
|
|
149
|
-
})
|
|
150
|
-
);
|
|
151
|
-
var sendReadiness = (identity, request) => Effect2.tryPromise({
|
|
152
|
-
try: () => new Promise((resolve2, reject) => {
|
|
153
|
-
const socket = createConnection(identity.socketPath);
|
|
154
|
-
const decoder = new StringDecoder("utf8");
|
|
155
|
-
let input = "";
|
|
156
|
-
let settled = false;
|
|
157
|
-
const finish = (complete) => {
|
|
158
|
-
if (settled) return;
|
|
159
|
-
settled = true;
|
|
160
|
-
clearTimeout(timer);
|
|
161
|
-
socket.destroy();
|
|
162
|
-
complete();
|
|
163
|
-
};
|
|
164
|
-
const timer = setTimeout(
|
|
165
|
-
() => finish(
|
|
166
|
-
() => reject(new Error("Daemon readiness request timed out"))
|
|
167
|
-
),
|
|
168
|
-
RESPONSE_TIMEOUT_MS
|
|
169
|
-
);
|
|
170
|
-
socket.once(
|
|
171
|
-
"connect",
|
|
172
|
-
() => socket.write(`${JSON.stringify(request)}
|
|
173
|
-
`)
|
|
174
|
-
);
|
|
175
|
-
socket.on("data", (chunk) => {
|
|
176
|
-
input += decoder.write(chunk);
|
|
177
|
-
if (Buffer.byteLength(input) > MAX_RESPONSE_BYTES) {
|
|
178
|
-
finish(
|
|
179
|
-
() => reject(new Error("Daemon readiness response is too large"))
|
|
180
|
-
);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
const boundary = input.indexOf("\n");
|
|
184
|
-
if (boundary !== -1) finish(() => resolve2(input.slice(0, boundary)));
|
|
185
|
-
});
|
|
186
|
-
socket.once("error", (cause) => finish(() => reject(cause)));
|
|
187
|
-
socket.once(
|
|
188
|
-
"close",
|
|
189
|
-
() => finish(
|
|
190
|
-
() => reject(new Error("Daemon closed before acknowledging readiness"))
|
|
191
|
-
)
|
|
192
|
-
);
|
|
193
|
-
}),
|
|
194
|
-
catch: (cause) => new Error("Could not report readiness to daemon", { cause })
|
|
195
|
-
}).pipe(
|
|
196
|
-
Effect2.flatMap(Schema.decodeUnknownEffect(DaemonResponse)),
|
|
197
|
-
Effect2.flatMap(
|
|
198
|
-
(response) => response.requestId === request.requestId && response.ok ? Effect2.void : Effect2.fail(new Error("Daemon did not acknowledge readiness"))
|
|
199
|
-
)
|
|
200
|
-
);
|
|
201
|
-
var publishDaemonReadiness = (value) => Effect2.suspend(() => {
|
|
202
|
-
const socketPath = process.env.DEVSESS_SOCKET;
|
|
203
|
-
const runId = process.env.DEVSESS_RUN_ID;
|
|
204
|
-
const service = process.env.DEVSESS_SERVICE;
|
|
205
|
-
if (socketPath === void 0 || runId === void 0 || service === void 0)
|
|
206
|
-
return Effect2.void;
|
|
207
|
-
const identity = { socketPath, runId, service };
|
|
208
|
-
return Effect2.acquireRelease(
|
|
209
|
-
Schema.decodeUnknownEffect(Schema.Json)(value).pipe(
|
|
210
|
-
Effect2.flatMap(
|
|
211
|
-
(json) => sendReadiness(identity, {
|
|
212
|
-
version: 1,
|
|
213
|
-
requestId: crypto.randomUUID(),
|
|
214
|
-
method: "publish",
|
|
215
|
-
params: { runId, service, value: json }
|
|
216
|
-
})
|
|
217
|
-
),
|
|
218
|
-
Effect2.exit
|
|
219
|
-
),
|
|
220
|
-
(result) => Exit.isSuccess(result) ? sendReadiness(identity, {
|
|
221
|
-
version: 1,
|
|
222
|
-
requestId: crypto.randomUUID(),
|
|
223
|
-
method: "unpublish",
|
|
224
|
-
params: { runId, service }
|
|
225
|
-
}).pipe(Effect2.catch(() => Effect2.void)) : Effect2.void
|
|
226
|
-
).pipe(Effect2.asVoid);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// src/dev/running-signal.ts
|
|
230
|
-
var RUNNING_SIGNAL_FILE = ".data/running.json";
|
|
231
|
-
var runningSignalPath = (dir) => join(dir, RUNNING_SIGNAL_FILE);
|
|
232
|
-
var resolveSiblingDir = (spec, fromDir) => {
|
|
233
|
-
if (spec.startsWith(".") || spec.startsWith("/")) {
|
|
234
|
-
return resolve(fromDir, spec);
|
|
235
|
-
}
|
|
236
|
-
const require2 = createRequire(join(fromDir, "package.json"));
|
|
237
|
-
const pkgJsonPath = require2.resolve(`${spec}/package.json`);
|
|
238
|
-
return dirname(realpathSync(pkgJsonPath));
|
|
239
|
-
};
|
|
240
|
-
var publishRunningSignal = (filePath, value) => Effect3.acquireRelease(
|
|
241
|
-
Effect3.gen(function* () {
|
|
242
|
-
const fs = yield* FileSystem2;
|
|
243
|
-
const path = yield* Path2;
|
|
244
|
-
const tmp = `${filePath}.tmp`;
|
|
245
|
-
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
|
|
246
|
-
yield* fs.writeFileString(tmp, JSON.stringify(value));
|
|
247
|
-
yield* fs.rename(tmp, filePath);
|
|
248
|
-
}),
|
|
249
|
-
() => Effect3.gen(function* () {
|
|
250
|
-
const fs = yield* FileSystem2;
|
|
251
|
-
yield* fs.remove(filePath).pipe(Effect3.catch(() => Effect3.void));
|
|
252
|
-
})
|
|
253
|
-
);
|
|
254
|
-
var awaitRunningSignal = (filePath, opts) => Effect3.scoped(
|
|
255
|
-
Effect3.gen(function* () {
|
|
256
|
-
const fs = yield* FileSystem2;
|
|
257
|
-
const path = yield* Path2;
|
|
258
|
-
const dir = path.dirname(filePath);
|
|
259
|
-
const filename = path.basename(filePath);
|
|
260
|
-
const deferred = yield* Deferred.make();
|
|
261
|
-
const tryRead = Effect3.gen(function* () {
|
|
262
|
-
if (!(yield* fs.exists(filePath))) return false;
|
|
263
|
-
const raw = yield* fs.readFileString(filePath).pipe(Effect3.catch(() => Effect3.succeed(null)));
|
|
264
|
-
if (raw == null) return false;
|
|
265
|
-
const parsed = yield* Effect3.try(() => opts.parse(raw)).pipe(
|
|
266
|
-
Effect3.catch(() => Effect3.succeed(null))
|
|
267
|
-
);
|
|
268
|
-
if (parsed == null) return false;
|
|
269
|
-
yield* Deferred.succeed(deferred, parsed);
|
|
270
|
-
return true;
|
|
271
|
-
});
|
|
272
|
-
yield* fs.makeDirectory(dir, { recursive: true });
|
|
273
|
-
yield* Effect3.acquireRelease(
|
|
274
|
-
Effect3.sync(
|
|
275
|
-
() => (
|
|
276
|
-
// Watch parent dir — the file may not exist yet, and watching a missing file errors.
|
|
277
|
-
fsWatch(dir, { recursive: false }, (eventType, eventFilename) => {
|
|
278
|
-
if (eventFilename !== filename) return;
|
|
279
|
-
if (eventType !== "rename" && eventType !== "change") return;
|
|
280
|
-
Effect3.runFork(tryRead);
|
|
281
|
-
})
|
|
282
|
-
)
|
|
283
|
-
),
|
|
284
|
-
(watcher) => Effect3.sync(() => watcher.close())
|
|
285
|
-
);
|
|
286
|
-
yield* tryRead;
|
|
287
|
-
return yield* Deferred.await(deferred);
|
|
288
|
-
})
|
|
289
|
-
);
|
|
290
|
-
var publishRunning = (value) => Effect3.gen(function* () {
|
|
291
|
-
const sessions = yield* DevSessions;
|
|
292
|
-
yield* publishRunningSignal(runningSignalPath(sessions.dir), value);
|
|
293
|
-
yield* publishDaemonReadiness(value);
|
|
294
|
-
});
|
|
295
|
-
var awaitRunning = (pkg) => Effect3.gen(function* () {
|
|
296
|
-
const sessions = yield* DevSessions;
|
|
297
|
-
return yield* awaitRunningSignal(
|
|
298
|
-
runningSignalPath(resolveSiblingDir(pkg, sessions.dir)),
|
|
299
|
-
{ parse: (raw) => JSON.parse(raw) }
|
|
300
|
-
);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// src/dev/session-state.ts
|
|
304
|
-
import { Data as Data2, Effect as Effect4 } from "effect";
|
|
305
|
-
import { FileSystem as FileSystem3 } from "effect/FileSystem";
|
|
306
|
-
import { Path as Path3 } from "effect/Path";
|
|
307
144
|
import * as S from "effect/Schema";
|
|
308
145
|
var SessionStateError = class extends Data2.TaggedError("SessionStateError") {
|
|
309
146
|
};
|
|
@@ -311,8 +148,8 @@ function getPath(session) {
|
|
|
311
148
|
return session.path("sess.json");
|
|
312
149
|
}
|
|
313
150
|
function read(filePath) {
|
|
314
|
-
return
|
|
315
|
-
const fs = yield*
|
|
151
|
+
return Effect2.gen(function* () {
|
|
152
|
+
const fs = yield* FileSystem2;
|
|
316
153
|
if (!(yield* fs.exists(filePath))) {
|
|
317
154
|
return null;
|
|
318
155
|
}
|
|
@@ -323,21 +160,21 @@ var SessionState;
|
|
|
323
160
|
((SessionState2) => {
|
|
324
161
|
function slot(schema) {
|
|
325
162
|
return {
|
|
326
|
-
read: (session) =>
|
|
163
|
+
read: (session) => Effect2.gen(function* () {
|
|
327
164
|
const filePath = yield* getPath(session);
|
|
328
165
|
const content = yield* read(filePath);
|
|
329
166
|
if (content == null) return null;
|
|
330
167
|
return yield* S.decodeEffect(S.fromJsonString(schema))(content).pipe(
|
|
331
|
-
|
|
168
|
+
Effect2.catchTag("SchemaError", () => Effect2.succeed(null))
|
|
332
169
|
);
|
|
333
170
|
}),
|
|
334
|
-
write: (session, data) =>
|
|
335
|
-
const fs = yield*
|
|
336
|
-
const path = yield*
|
|
171
|
+
write: (session, data) => Effect2.gen(function* () {
|
|
172
|
+
const fs = yield* FileSystem2;
|
|
173
|
+
const path = yield* Path2;
|
|
337
174
|
const filePath = yield* getPath(session);
|
|
338
175
|
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
|
|
339
176
|
const existing = yield* read(filePath);
|
|
340
|
-
const parsed = yield*
|
|
177
|
+
const parsed = yield* Effect2.try({
|
|
341
178
|
try: () => JSON.parse(existing ?? "{}"),
|
|
342
179
|
catch: (cause) => new SessionStateError({
|
|
343
180
|
message: `Failed to parse ${filePath} as JSON`,
|
|
@@ -353,9 +190,31 @@ var SessionState;
|
|
|
353
190
|
})(SessionState || (SessionState = {}));
|
|
354
191
|
|
|
355
192
|
// src/dev/sticky-port.ts
|
|
356
|
-
import { Effect as
|
|
193
|
+
import { Effect as Effect4 } from "effect";
|
|
194
|
+
import { FileSystem as FileSystem3 } from "effect/FileSystem";
|
|
195
|
+
import { Path as Path3 } from "effect/Path";
|
|
357
196
|
import * as S2 from "effect/Schema";
|
|
358
197
|
import getPort from "get-port";
|
|
198
|
+
|
|
199
|
+
// src/lock.ts
|
|
200
|
+
import { Effect as Effect3, Schedule } from "effect";
|
|
201
|
+
import lockfile from "proper-lockfile";
|
|
202
|
+
var acquireFileLock = (file, lockPath = `${file}.lock`) => Effect3.tryPromise({
|
|
203
|
+
try: () => lockfile.lock(file, {
|
|
204
|
+
realpath: false,
|
|
205
|
+
lockfilePath: lockPath,
|
|
206
|
+
stale: 6e4,
|
|
207
|
+
retries: 0
|
|
208
|
+
}),
|
|
209
|
+
catch: (cause) => new Error(`Failed to lock ${file}`, { cause })
|
|
210
|
+
}).pipe(
|
|
211
|
+
Effect3.retry({
|
|
212
|
+
while: (error) => typeof error.cause === "object" && error.cause !== null && "code" in error.cause && error.cause.code === "ELOCKED",
|
|
213
|
+
schedule: Schedule.spaced("50 millis")
|
|
214
|
+
})
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// src/dev/sticky-port.ts
|
|
359
218
|
var DEFAULT_NAME = "default";
|
|
360
219
|
var StickyPorts = SessionState.slot(
|
|
361
220
|
S2.Struct({
|
|
@@ -363,13 +222,28 @@ var StickyPorts = SessionState.slot(
|
|
|
363
222
|
port: S2.optional(S2.Number)
|
|
364
223
|
})
|
|
365
224
|
);
|
|
366
|
-
var getStickyPort = (session, options) =>
|
|
225
|
+
var getStickyPort = (session, options) => Effect4.gen(function* () {
|
|
367
226
|
const name = options?.name ?? DEFAULT_NAME;
|
|
227
|
+
const fs = yield* FileSystem3;
|
|
228
|
+
const path = yield* Path3;
|
|
229
|
+
const statePath = yield* session.path("sess.json");
|
|
230
|
+
yield* fs.makeDirectory(path.dirname(statePath), { recursive: true });
|
|
231
|
+
yield* Effect4.acquireRelease(
|
|
232
|
+
acquireFileLock(statePath).pipe(
|
|
233
|
+
Effect4.mapError(
|
|
234
|
+
(cause) => new SessionStateError({
|
|
235
|
+
message: "Failed to lock sticky ports",
|
|
236
|
+
cause
|
|
237
|
+
})
|
|
238
|
+
)
|
|
239
|
+
),
|
|
240
|
+
(release) => Effect4.promise(() => release())
|
|
241
|
+
);
|
|
368
242
|
const file = yield* StickyPorts.read(session);
|
|
369
243
|
const ports = file?.ports ?? (file?.port !== void 0 ? { [DEFAULT_NAME]: file.port } : {});
|
|
370
244
|
const preferred = ports[name];
|
|
371
245
|
const exclude = Object.entries(ports).filter((entry) => entry[0] !== name).map((entry) => entry[1]);
|
|
372
|
-
const port = yield*
|
|
246
|
+
const port = yield* Effect4.promise(
|
|
373
247
|
async () => getPort({
|
|
374
248
|
port: preferred !== void 0 ? [preferred] : void 0,
|
|
375
249
|
...exclude.length > 0 ? { exclude } : {}
|
|
@@ -377,53 +251,112 @@ var getStickyPort = (session, options) => Effect5.gen(function* () {
|
|
|
377
251
|
);
|
|
378
252
|
yield* StickyPorts.write(session, { ports: { ...ports, [name]: port } });
|
|
379
253
|
return port;
|
|
254
|
+
}).pipe(Effect4.scoped);
|
|
255
|
+
|
|
256
|
+
// src/socket-line.ts
|
|
257
|
+
import { StringDecoder } from "string_decoder";
|
|
258
|
+
var readSocketLine = (socket, maxBytes = 1048576) => new Promise((resolve, reject) => {
|
|
259
|
+
const decoder = new StringDecoder("utf8");
|
|
260
|
+
const state = { input: "", bytes: 0 };
|
|
261
|
+
const cleanup = () => {
|
|
262
|
+
socket.removeListener("data", data);
|
|
263
|
+
socket.removeListener("error", error);
|
|
264
|
+
socket.removeListener("close", closed);
|
|
265
|
+
};
|
|
266
|
+
const error = (cause) => {
|
|
267
|
+
cleanup();
|
|
268
|
+
reject(cause);
|
|
269
|
+
};
|
|
270
|
+
const closed = () => error(new Error("Socket closed before sending a ready line"));
|
|
271
|
+
const data = (chunk) => {
|
|
272
|
+
const boundary = chunk.indexOf(10);
|
|
273
|
+
const part = boundary < 0 ? chunk : chunk.subarray(0, boundary);
|
|
274
|
+
state.bytes += part.length;
|
|
275
|
+
if (state.bytes > maxBytes)
|
|
276
|
+
return error(new Error("Socket line exceeds byte limit"));
|
|
277
|
+
state.input += decoder.write(part);
|
|
278
|
+
if (boundary >= 0) {
|
|
279
|
+
cleanup();
|
|
280
|
+
resolve(state.input + decoder.end());
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
socket.on("data", data);
|
|
284
|
+
socket.once("error", error);
|
|
285
|
+
socket.once("close", closed);
|
|
380
286
|
});
|
|
381
287
|
|
|
382
|
-
// src/dev/
|
|
383
|
-
import {
|
|
384
|
-
import {
|
|
385
|
-
var
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
);
|
|
412
|
-
return yield* child.exitCode;
|
|
288
|
+
// src/dev/daemon-client.ts
|
|
289
|
+
import { createConnection } from "net";
|
|
290
|
+
import { Config, ConfigProvider, Effect as Effect5, Option as Option2, Schema } from "effect";
|
|
291
|
+
var RESPONSE_TIMEOUT_MS = 5e3;
|
|
292
|
+
var MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
293
|
+
var DaemonResponse = Schema.fromJsonString(
|
|
294
|
+
Schema.Struct({
|
|
295
|
+
version: Schema.Literal(1),
|
|
296
|
+
requestId: Schema.NonEmptyString,
|
|
297
|
+
ok: Schema.Boolean,
|
|
298
|
+
result: Schema.optional(Schema.Unknown),
|
|
299
|
+
error: Schema.optional(Schema.String)
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
var readDaemonIdentity = Effect5.gen(function* () {
|
|
303
|
+
const provider = ConfigProvider.fromEnv();
|
|
304
|
+
const read2 = (key) => Config.String(key).pipe(Config.option).parse(provider).pipe(Effect5.map(Option2.getOrUndefined));
|
|
305
|
+
const socketPath = yield* read2("DEVSESS_SOCKET");
|
|
306
|
+
const runId = yield* read2("DEVSESS_RUN_ID");
|
|
307
|
+
const service = yield* read2("DEVSESS_SERVICE");
|
|
308
|
+
const instanceId = yield* read2("DEVSESS_INSTANCE_ID");
|
|
309
|
+
if (socketPath === void 0 || runId === void 0 || service === void 0)
|
|
310
|
+
return void 0;
|
|
311
|
+
return {
|
|
312
|
+
socketPath,
|
|
313
|
+
runId,
|
|
314
|
+
service,
|
|
315
|
+
...instanceId === void 0 ? {} : { instanceId }
|
|
316
|
+
};
|
|
413
317
|
});
|
|
318
|
+
var sendDaemonRequest = (identity, request, timeoutMs = RESPONSE_TIMEOUT_MS) => Effect5.tryPromise({
|
|
319
|
+
try: () => new Promise((resolve, reject) => {
|
|
320
|
+
const socket = createConnection(identity.socketPath);
|
|
321
|
+
let settled = false;
|
|
322
|
+
const finish = (complete) => {
|
|
323
|
+
if (settled) return;
|
|
324
|
+
settled = true;
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
socket.destroy();
|
|
327
|
+
complete();
|
|
328
|
+
};
|
|
329
|
+
const timer = setTimeout(
|
|
330
|
+
() => finish(() => reject(new Error("Daemon request timed out"))),
|
|
331
|
+
timeoutMs
|
|
332
|
+
);
|
|
333
|
+
socket.once(
|
|
334
|
+
"connect",
|
|
335
|
+
() => socket.write(`${JSON.stringify(request)}
|
|
336
|
+
`)
|
|
337
|
+
);
|
|
338
|
+
readSocketLine(socket, MAX_RESPONSE_BYTES).then(
|
|
339
|
+
(line) => finish(() => resolve(line)),
|
|
340
|
+
(cause) => finish(() => reject(cause))
|
|
341
|
+
);
|
|
342
|
+
}),
|
|
343
|
+
catch: (cause) => new Error("Could not send request to daemon", { cause })
|
|
344
|
+
}).pipe(
|
|
345
|
+
Effect5.flatMap(Schema.decodeUnknownEffect(DaemonResponse)),
|
|
346
|
+
Effect5.flatMap(
|
|
347
|
+
(response) => response.requestId === request.requestId && response.ok ? Effect5.void : Effect5.fail(new Error("Daemon did not acknowledge request"))
|
|
348
|
+
)
|
|
349
|
+
);
|
|
414
350
|
|
|
415
351
|
export {
|
|
416
352
|
DevSessions,
|
|
417
353
|
ProjectRootNotFoundError,
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
awaitRunningSignal,
|
|
422
|
-
publishRunning,
|
|
423
|
-
awaitRunning,
|
|
354
|
+
readSocketLine,
|
|
355
|
+
readDaemonIdentity,
|
|
356
|
+
sendDaemonRequest,
|
|
424
357
|
SessionStateError,
|
|
425
358
|
SessionState,
|
|
426
|
-
|
|
427
|
-
|
|
359
|
+
acquireFileLock,
|
|
360
|
+
getStickyPort
|
|
428
361
|
};
|
|
429
|
-
//# sourceMappingURL=chunk-
|
|
362
|
+
//# sourceMappingURL=chunk-MBPIOYW3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dev-sessions.ts","../src/dev/session-state.ts","../src/dev/sticky-port.ts","../src/lock.ts","../src/socket-line.ts","../src/dev/daemon-client.ts"],"sourcesContent":["import { Array as A, Context, Data, Effect, Layer, Option } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport type { PlatformError } from 'effect/PlatformError';\nimport { generateSlug } from 'random-word-slugs';\n\nnamespace DevSession {\n\ttype DevSessionInput = {\n\t\tname: string;\n\t\tlastModifiedAt: Date | null;\n\t\trootDir: string;\n\t\tpathJoin: (relativePath: string) => string;\n\t};\n\n\texport function make(input: DevSessionInput) {\n\t\treturn {\n\t\t\trootDir: input.rootDir,\n\t\t\tname: input.name,\n\t\t\tlastModifiedAt: input.lastModifiedAt,\n\t\t\tpath: (relativePath: string) =>\n\t\t\t\tEffect.succeed(input.pathJoin(relativePath)),\n\t\t\ttoString: () => input.name,\n\t\t};\n\t}\n}\n\nexport type DevSession = ReturnType<typeof DevSession.make>;\n\nexport class DevSessions extends Context.Service<\n\tDevSessions,\n\t{\n\t\treadonly dir: string;\n\t\treadonly path: (relativePath: string) => string;\n\t\treadonly getSessions: Effect.Effect<Array<DevSession>, PlatformError>;\n\t\treadonly createSession: Effect.Effect<DevSession, PlatformError>;\n\t\treadonly getLatestOrCreate: Effect.Effect<DevSession, PlatformError>;\n\t\treadonly getOrCreate: (\n\t\t\tname: string,\n\t\t) => Effect.Effect<DevSession, PlatformError>;\n\t}\n>()('devsess/DevSessions') {\n\t/**\n\t * `DevSessions` explicitly rooted at `rootDir`: `dir`/`path` resolve against `rootDir`\n\t * itself (e.g. `sessions.path('drizzle')` reaches a project's real `drizzle/` folder),\n\t * while session directories are namespaced under `<rootDir>/.data/sessions`.\n\t *\n\t * Deliberately does *not* also provide `CurrentSession` — a `Layer`'s build effect\n\t * runs as soon as it's provided, before anything downstream (e.g. `cli.Command.run`'s\n\t * argv parsing) gets a say. Bundling session resolution in here would create a\n\t * session for `--help`/`--version`/a bad flag too, since those never reach a command\n\t * handler at all. Provide `CurrentSession.layer` around the handler itself instead.\n\t */\n\tstatic readonly layerAt = (rootDir: string) => buildDevSessionsLayer(rootDir);\n\n\t/**\n\t * `DevSessions.layerAt` rooted at the auto-detected project root — the nearest\n\t * ancestor of `process.cwd()` containing a `package.json`.\n\t */\n\tstatic readonly layer: Layer.Layer<\n\t\tDevSessions,\n\t\tPlatformError | ProjectRootNotFoundError,\n\t\tFileSystem | Path\n\t> = Layer.unwrap(\n\t\tEffect.suspend(() =>\n\t\t\tEffect.map(findProjectRoot, (rootDir) => DevSessions.layerAt(rootDir)),\n\t\t),\n\t);\n}\n\n/**\n * Raised by `DevSessions.layer` when no ancestor of `searchedFrom` contains a\n * `package.json`. Failing loudly beats silently falling back to `process.cwd()` — a\n * wrong root means dev sessions get written to a surprising place.\n */\nexport class ProjectRootNotFoundError extends Data.TaggedError(\n\t'ProjectRootNotFoundError',\n)<{\n\treadonly searchedFrom: string;\n}> {}\n\n/** Walks up from `process.cwd()` to the nearest ancestor containing a `package.json`. */\nconst findProjectRoot = Effect.gen(function* () {\n\tconst fs = yield* FileSystem;\n\tconst path = yield* Path;\n\tconst startDir = process.cwd();\n\n\tconst walkUp = (\n\t\tdir: string,\n\t): Effect.Effect<string, PlatformError | ProjectRootNotFoundError> =>\n\t\tEffect.gen(function* () {\n\t\t\tif (yield* fs.exists(path.join(dir, 'package.json'))) {\n\t\t\t\treturn dir;\n\t\t\t}\n\t\t\tconst parent = path.dirname(dir);\n\t\t\tif (parent === dir) {\n\t\t\t\treturn yield* Effect.fail(\n\t\t\t\t\tnew ProjectRootNotFoundError({ searchedFrom: startDir }),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn yield* walkUp(parent);\n\t\t});\n\n\treturn yield* walkUp(startDir);\n});\n\n/** The listing/creation logic shared by every `DevSessions` layer, parameterized on the literal directory session subdirs live under. */\nconst buildDevSessionsCore = (sessionsDir: string, rootDir: string) =>\n\tEffect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tconst path = yield* Path;\n\n\t\tyield* Effect.logDebug(`DevSessions dir: ${sessionsDir}`);\n\n\t\tconst getSessions = Effect.gen(function* () {\n\t\t\tconst entries = yield* fs\n\t\t\t\t.readDirectory(sessionsDir)\n\t\t\t\t.pipe(Effect.catch(() => Effect.succeed([] as Array<string>)));\n\n\t\t\tconst possibleSessions = yield* Effect.all(\n\t\t\t\tentries.map((entry) =>\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tconst stat = yield* fs.stat(path.join(sessionsDir, entry));\n\t\t\t\t\t\tif (stat.type !== 'Directory') return Option.none();\n\t\t\t\t\t\treturn Option.some(\n\t\t\t\t\t\t\tDevSession.make({\n\t\t\t\t\t\t\t\tname: entry,\n\t\t\t\t\t\t\t\tlastModifiedAt: Option.getOrNull(stat.mtime),\n\t\t\t\t\t\t\t\trootDir,\n\t\t\t\t\t\t\t\tpathJoin: (relativePath: string) =>\n\t\t\t\t\t\t\t\t\tpath.join(sessionsDir, entry, relativePath),\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t);\n\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t\t{ concurrency: 'unbounded' },\n\t\t\t);\n\n\t\t\treturn A.getSomes(possibleSessions);\n\t\t});\n\n\t\tconst createSession = Effect.gen(function* () {\n\t\t\tconst slug = generateSlug(1, { partsOfSpeech: ['noun'] });\n\t\t\tyield* fs.makeDirectory(path.join(sessionsDir, slug), {\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn DevSession.make({\n\t\t\t\tname: slug,\n\t\t\t\tlastModifiedAt: null,\n\t\t\t\trootDir,\n\t\t\t\tpathJoin: (relativePath: string) =>\n\t\t\t\t\tpath.join(sessionsDir, slug, relativePath),\n\t\t\t});\n\t\t});\n\n\t\tconst getLatestOrCreate = Effect.gen(function* () {\n\t\t\tyield* fs.makeDirectory(sessionsDir, { recursive: true });\n\n\t\t\tconst sessions = yield* getSessions;\n\t\t\tif (sessions.length > 0) {\n\t\t\t\tsessions.sort(\n\t\t\t\t\t(a, b) =>\n\t\t\t\t\t\t(b.lastModifiedAt?.getTime() ?? 0) -\n\t\t\t\t\t\t(a.lastModifiedAt?.getTime() ?? 0),\n\t\t\t\t);\n\t\t\t\tconst latest = sessions[0];\n\t\t\t\treturn latest as DevSession;\n\t\t\t}\n\n\t\t\treturn yield* createSession;\n\t\t});\n\n\t\tconst getOrCreate = (name: string) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tyield* fs.makeDirectory(path.join(sessionsDir, name), {\n\t\t\t\t\trecursive: true,\n\t\t\t\t});\n\t\t\t\treturn DevSession.make({\n\t\t\t\t\tname,\n\t\t\t\t\trootDir,\n\t\t\t\t\tlastModifiedAt: null,\n\t\t\t\t\tpathJoin: (relativePath) =>\n\t\t\t\t\t\tpath.join(sessionsDir, name, relativePath),\n\t\t\t\t});\n\t\t\t});\n\t\treturn { getSessions, createSession, getLatestOrCreate, getOrCreate };\n\t});\n\n/**\n * The one `DevSessions` layer constructor: `dir`/`path` resolve against `rootDir`\n * itself, while session directories are namespaced under `<rootDir>/.data/sessions` —\n * the library owns that layout, so every caller (`DevSessions.layerAt`, the async\n * facade's `createDevSessions`) means the same thing by `rootDir`.\n */\nconst buildDevSessionsLayer = (rootDir: string) =>\n\tLayer.effect(\n\t\tDevSessions,\n\t\tEffect.gen(function* () {\n\t\t\tconst path = yield* Path;\n\t\t\tconst sessionsDir = path.join(rootDir, '.data', 'sessions');\n\t\t\tconst core = yield* buildDevSessionsCore(sessionsDir, rootDir);\n\t\t\treturn {\n\t\t\t\tdir: rootDir,\n\t\t\t\tpath: (relativePath: string) => path.join(rootDir, relativePath),\n\t\t\t\t...core,\n\t\t\t};\n\t\t}),\n\t);\n","import { Data, Effect } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport * as S from 'effect/Schema';\nimport type { DevSession } from '../dev-sessions';\n\nexport class SessionStateError extends Data.TaggedError('SessionStateError')<{\n\tmessage: string;\n\tcause?: unknown;\n}> {}\n\nfunction getPath(session: DevSession) {\n\treturn session.path('sess.json');\n}\n\nfunction read(filePath: string) {\n\treturn Effect.gen(function* () {\n\t\tconst fs = yield* FileSystem;\n\t\tif (!(yield* fs.exists(filePath))) {\n\t\t\treturn null;\n\t\t}\n\t\treturn yield* fs.readFileString(filePath);\n\t});\n}\n\nexport namespace SessionState {\n\ttype Shape = S.Top;\n\n\texport function slot<T extends Shape>(schema: T) {\n\t\treturn {\n\t\t\tread: (session: DevSession) =>\n\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\tconst filePath = yield* getPath(session);\n\t\t\t\t\tconst content = yield* read(filePath);\n\t\t\t\t\tif (content == null) return null;\n\t\t\t\t\treturn yield* S.decodeEffect(S.fromJsonString(schema))(content).pipe(\n\t\t\t\t\t\tEffect.catchTag('SchemaError', () => Effect.succeed(null)),\n\t\t\t\t\t);\n\t\t\t\t}),\n\t\t\twrite: (session: DevSession, data: T['Type']) =>\n\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\tconst fs = yield* FileSystem;\n\t\t\t\t\tconst path = yield* Path;\n\t\t\t\t\tconst filePath = yield* getPath(session);\n\t\t\t\t\tyield* fs.makeDirectory(path.dirname(filePath), { recursive: true });\n\t\t\t\t\tconst existing = yield* read(filePath);\n\t\t\t\t\tconst parsed = yield* Effect.try({\n\t\t\t\t\t\ttry: () => JSON.parse(existing ?? '{}') as Record<string, unknown>,\n\t\t\t\t\t\tcatch: (cause) =>\n\t\t\t\t\t\t\tnew SessionStateError({\n\t\t\t\t\t\t\t\tmessage: `Failed to parse ${filePath} as JSON`,\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t});\n\t\t\t\t\tconst merged = { ...parsed, ...(data as Record<string, unknown>) };\n\t\t\t\t\tyield* fs.writeFileString(filePath, JSON.stringify(merged));\n\t\t\t\t}),\n\t\t};\n\t}\n}\n","import { Effect } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport * as S from 'effect/Schema';\nimport getPort from 'get-port';\nimport type { DevSession } from '../dev-sessions';\nimport { acquireFileLock } from '../lock';\nimport { SessionState, SessionStateError } from './session-state';\n\nconst DEFAULT_NAME = 'default';\n\n/**\n * Reads both the current `{ ports: Record<string, number> }` shape and the legacy\n * top-level `{ port: number }` shape, so a session written before named ports existed\n * still yields its remembered port (as `default`). Writes only ever use `ports` —\n * the legacy `port` key is never touched again once the new shape is written.\n */\nconst StickyPorts = SessionState.slot(\n\tS.Struct({\n\t\tports: S.optional(S.Record(S.String, S.Number)),\n\t\tport: S.optional(S.Number),\n\t}),\n);\n\nexport const getStickyPort = (\n\tsession: DevSession,\n\toptions?: { name?: string },\n) =>\n\tEffect.gen(function* () {\n\t\tconst name = options?.name ?? DEFAULT_NAME;\n\t\tconst fs = yield* FileSystem;\n\t\tconst path = yield* Path;\n\t\tconst statePath = yield* session.path('sess.json');\n\t\tyield* fs.makeDirectory(path.dirname(statePath), { recursive: true });\n\t\tyield* Effect.acquireRelease(\n\t\t\tacquireFileLock(statePath).pipe(\n\t\t\t\tEffect.mapError(\n\t\t\t\t\t(cause) =>\n\t\t\t\t\t\tnew SessionStateError({\n\t\t\t\t\t\t\tmessage: 'Failed to lock sticky ports',\n\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t}),\n\t\t\t\t),\n\t\t\t),\n\t\t\t(release) => Effect.promise(() => release()),\n\t\t);\n\n\t\tconst file = yield* StickyPorts.read(session);\n\t\tconst ports =\n\t\t\tfile?.ports ??\n\t\t\t(file?.port !== undefined ? { [DEFAULT_NAME]: file.port } : {});\n\n\t\tconst preferred = ports[name];\n\t\tconst exclude = Object.entries(ports)\n\t\t\t.filter((entry) => entry[0] !== name)\n\t\t\t.map((entry) => entry[1]);\n\n\t\tconst port = yield* Effect.promise(async () =>\n\t\t\tgetPort({\n\t\t\t\tport: preferred !== undefined ? [preferred] : undefined,\n\t\t\t\t...(exclude.length > 0 ? { exclude } : {}),\n\t\t\t}),\n\t\t);\n\n\t\tyield* StickyPorts.write(session, { ports: { ...ports, [name]: port } });\n\n\t\treturn port;\n\t}).pipe(Effect.scoped);\n","import { Effect, Schedule } from 'effect';\nimport lockfile from 'proper-lockfile';\n\nexport const acquireFileLock = (file: string, lockPath = `${file}.lock`) =>\n\tEffect.tryPromise({\n\t\ttry: () =>\n\t\t\tlockfile.lock(file, {\n\t\t\t\trealpath: false,\n\t\t\t\tlockfilePath: lockPath,\n\t\t\t\tstale: 60_000,\n\t\t\t\tretries: 0,\n\t\t\t}),\n\t\tcatch: (cause) => new Error(`Failed to lock ${file}`, { cause }),\n\t}).pipe(\n\t\tEffect.retry({\n\t\t\twhile: (error) =>\n\t\t\t\ttypeof error.cause === 'object' &&\n\t\t\t\terror.cause !== null &&\n\t\t\t\t'code' in error.cause &&\n\t\t\t\terror.cause.code === 'ELOCKED',\n\t\t\tschedule: Schedule.spaced('50 millis'),\n\t\t}),\n\t);\n","import type { Socket } from 'node:net';\nimport { StringDecoder } from 'node:string_decoder';\n\nexport const readSocketLine = (\n\tsocket: Socket,\n\tmaxBytes = 1_048_576,\n): Promise<string> =>\n\tnew Promise((resolve, reject) => {\n\t\tconst decoder = new StringDecoder('utf8');\n\t\tconst state = { input: '', bytes: 0 };\n\t\tconst cleanup = () => {\n\t\t\tsocket.removeListener('data', data);\n\t\t\tsocket.removeListener('error', error);\n\t\t\tsocket.removeListener('close', closed);\n\t\t};\n\t\tconst error = (cause: Error) => {\n\t\t\tcleanup();\n\t\t\treject(cause);\n\t\t};\n\t\tconst closed = () =>\n\t\t\terror(new Error('Socket closed before sending a ready line'));\n\t\tconst data = (chunk: Buffer) => {\n\t\t\tconst boundary = chunk.indexOf(10);\n\t\t\tconst part = boundary < 0 ? chunk : chunk.subarray(0, boundary);\n\t\t\tstate.bytes += part.length;\n\t\t\tif (state.bytes > maxBytes)\n\t\t\t\treturn error(new Error('Socket line exceeds byte limit'));\n\t\t\tstate.input += decoder.write(part);\n\t\t\tif (boundary >= 0) {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(state.input + decoder.end());\n\t\t\t}\n\t\t};\n\t\tsocket.on('data', data);\n\t\tsocket.once('error', error);\n\t\tsocket.once('close', closed);\n\t});\n","import { createConnection } from 'node:net';\nimport { Config, ConfigProvider, Effect, Option, Schema } from 'effect';\nimport { readSocketLine } from '../socket-line';\n\n// The daemon serves requests one at a time, so a readiness report can queue\n// behind slow ones (e.g. `devsess start` polling the run list while it waits).\nconst RESPONSE_TIMEOUT_MS = 5_000;\nconst MAX_RESPONSE_BYTES = 1024 * 1024;\n\nconst DaemonResponse = Schema.fromJsonString(\n\tSchema.Struct({\n\t\tversion: Schema.Literal(1),\n\t\trequestId: Schema.NonEmptyString,\n\t\tok: Schema.Boolean,\n\t\tresult: Schema.optional(Schema.Unknown),\n\t\terror: Schema.optional(Schema.String),\n\t}),\n);\n\ntype ServiceIdentity = {\n\treadonly socketPath: string;\n\treadonly runId: string;\n\treadonly service: string;\n\treadonly instanceId?: string;\n};\n\nexport const readDaemonIdentity = Effect.gen(function* () {\n\tconst provider = ConfigProvider.fromEnv();\n\tconst read = (key: string) =>\n\t\tConfig.String(key)\n\t\t\t.pipe(Config.option)\n\t\t\t.parse(provider)\n\t\t\t.pipe(Effect.map(Option.getOrUndefined));\n\tconst socketPath = yield* read('DEVSESS_SOCKET');\n\tconst runId = yield* read('DEVSESS_RUN_ID');\n\tconst service = yield* read('DEVSESS_SERVICE');\n\tconst instanceId = yield* read('DEVSESS_INSTANCE_ID');\n\tif (socketPath === undefined || runId === undefined || service === undefined)\n\t\treturn undefined;\n\treturn {\n\t\tsocketPath,\n\t\trunId,\n\t\tservice,\n\t\t...(instanceId === undefined ? {} : { instanceId }),\n\t};\n});\n\nexport const sendDaemonRequest = (\n\tidentity: ServiceIdentity,\n\trequest: {\n\t\treadonly version: 1;\n\t\treadonly requestId: string;\n\t\treadonly method: string;\n\t\treadonly params: Readonly<Record<string, unknown>>;\n\t},\n\ttimeoutMs = RESPONSE_TIMEOUT_MS,\n) =>\n\tEffect.tryPromise({\n\t\ttry: () =>\n\t\t\tnew Promise<string>((resolve, reject) => {\n\t\t\t\tconst socket = createConnection(identity.socketPath);\n\t\t\t\tlet settled = false;\n\t\t\t\tconst finish = (complete: () => void) => {\n\t\t\t\t\tif (settled) return;\n\t\t\t\t\tsettled = true;\n\t\t\t\t\tclearTimeout(timer);\n\t\t\t\t\tsocket.destroy();\n\t\t\t\t\tcomplete();\n\t\t\t\t};\n\t\t\t\tconst timer = setTimeout(\n\t\t\t\t\t() => finish(() => reject(new Error('Daemon request timed out'))),\n\t\t\t\t\ttimeoutMs,\n\t\t\t\t);\n\t\t\t\tsocket.once('connect', () =>\n\t\t\t\t\tsocket.write(`${JSON.stringify(request)}\\n`),\n\t\t\t\t);\n\t\t\t\treadSocketLine(socket, MAX_RESPONSE_BYTES).then(\n\t\t\t\t\t(line) => finish(() => resolve(line)),\n\t\t\t\t\t(cause) => finish(() => reject(cause)),\n\t\t\t\t);\n\t\t\t}),\n\t\tcatch: (cause) => new Error('Could not send request to daemon', { cause }),\n\t}).pipe(\n\t\tEffect.flatMap(Schema.decodeUnknownEffect(DaemonResponse)),\n\t\tEffect.flatMap((response) =>\n\t\t\tresponse.requestId === request.requestId && response.ok\n\t\t\t\t? Effect.void\n\t\t\t\t: Effect.fail(new Error('Daemon did not acknowledge request')),\n\t\t),\n\t);\n"],"mappings":"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","names":["DevSession","Data","Effect","FileSystem","Path","SessionState","Effect","FileSystem","Path","S","Effect","Effect","FileSystem","Path","Effect","Option","Effect","read","Option"]}
|