@sma1lboy/kobe 0.5.12 → 0.5.13

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/dist/bin/kobed.js CHANGED
@@ -99,30 +99,36 @@ class KobeDaemonClient {
99
99
  nextId = 1;
100
100
  pending = new Map;
101
101
  handlers = new Map;
102
+ lifecycleHandlers = new Map;
103
+ connecting = null;
104
+ disposed = false;
102
105
  constructor(socketPath) {
103
106
  this.socketPath = socketPath;
104
107
  }
105
108
  connect() {
106
109
  if (this.socket)
107
110
  return Promise.resolve();
108
- return new Promise((resolve, reject) => {
109
- const socket = connect(this.socketPath);
110
- this.socket = socket;
111
- socket.once("connect", resolve);
112
- socket.once("error", reject);
113
- socket.on("data", (chunk) => this.onData(chunk.toString("utf8")));
114
- socket.on("close", () => {
115
- this.socket = null;
116
- for (const pending of this.pending.values())
117
- pending.reject(new Error("daemon connection closed"));
118
- this.pending.clear();
119
- });
120
- });
111
+ if (this.disposed)
112
+ return Promise.reject(new Error("daemon client disposed"));
113
+ if (this.connecting)
114
+ return this.connecting;
115
+ const p = this.openSocket();
116
+ this.connecting = p;
117
+ const cleanup = () => {
118
+ if (this.connecting === p)
119
+ this.connecting = null;
120
+ };
121
+ p.then(cleanup, cleanup);
122
+ return p;
121
123
  }
122
124
  close() {
125
+ this.disposed = true;
123
126
  this.socket?.end();
124
127
  this.socket = null;
125
128
  }
129
+ forceDisconnect() {
130
+ this.socket?.destroy();
131
+ }
126
132
  on(name, handler) {
127
133
  let set = this.handlers.get(name);
128
134
  if (!set) {
@@ -136,6 +142,19 @@ class KobeDaemonClient {
136
142
  this.handlers.delete(name);
137
143
  };
138
144
  }
145
+ onLifecycle(name, handler) {
146
+ let set = this.lifecycleHandlers.get(name);
147
+ if (!set) {
148
+ set = new Set;
149
+ this.lifecycleHandlers.set(name, set);
150
+ }
151
+ set.add(handler);
152
+ return () => {
153
+ set?.delete(handler);
154
+ if (set?.size === 0)
155
+ this.lifecycleHandlers.delete(name);
156
+ };
157
+ }
139
158
  async request(name, payload) {
140
159
  await this.connect();
141
160
  const socket = this.socket;
@@ -148,6 +167,44 @@ class KobeDaemonClient {
148
167
  socket.write(frameToLine({ type: "request", id, name, payload }));
149
168
  return promise;
150
169
  }
170
+ openSocket() {
171
+ return new Promise((resolve, reject) => {
172
+ const socket = connect(this.socketPath);
173
+ this.socket = socket;
174
+ const onConnect = () => {
175
+ socket.off("error", onError);
176
+ resolve();
177
+ };
178
+ const onError = (err) => {
179
+ socket.off("connect", onConnect);
180
+ if (this.socket === socket)
181
+ this.socket = null;
182
+ reject(err);
183
+ };
184
+ socket.once("connect", onConnect);
185
+ socket.once("error", onError);
186
+ socket.on("data", (chunk) => this.onData(chunk.toString("utf8")));
187
+ socket.on("close", () => this.onSocketClose(socket));
188
+ });
189
+ }
190
+ onSocketClose(which) {
191
+ if (this.socket !== which && this.socket !== null)
192
+ return;
193
+ this.socket = null;
194
+ for (const pending of this.pending.values())
195
+ pending.reject(new Error("daemon connection closed"));
196
+ this.pending.clear();
197
+ this.emitLifecycle("close");
198
+ }
199
+ emitLifecycle(name) {
200
+ for (const handler of this.lifecycleHandlers.get(name) ?? []) {
201
+ try {
202
+ handler();
203
+ } catch (err) {
204
+ console.error(`[kobe] lifecycle handler for "${name}" threw:`, err);
205
+ }
206
+ }
207
+ }
151
208
  onData(chunk) {
152
209
  this.buffer += chunk;
153
210
  let nl = this.buffer.indexOf(`
@@ -192,11 +249,10 @@ import { spawn } from "child_process";
192
249
  import { existsSync } from "fs";
193
250
  import { dirname, join as join2, resolve } from "path";
194
251
  import { fileURLToPath } from "url";
195
- async function connectOrStartDaemon() {
252
+ async function ensureDaemonReachable() {
196
253
  const socketPath = defaultDaemonSocketPath();
197
- const client = new KobeDaemonClient(socketPath);
198
- if (await canConnect(client))
199
- return client;
254
+ if (await testCanConnect(socketPath))
255
+ return socketPath;
200
256
  const { entry, runWithBun } = resolveKobedEntry();
201
257
  const child = runWithBun ? spawn(process.execPath, [entry, "start"], {
202
258
  detached: true,
@@ -211,24 +267,26 @@ async function connectOrStartDaemon() {
211
267
  const deadline = Date.now() + 5000;
212
268
  let lastErr;
213
269
  while (Date.now() < deadline) {
214
- const next = new KobeDaemonClient(socketPath);
215
- try {
216
- await next.connect();
217
- return next;
218
- } catch (err) {
219
- lastErr = err;
220
- next.close();
221
- await new Promise((resolve2) => setTimeout(resolve2, 100));
222
- }
270
+ if (await testCanConnect(socketPath))
271
+ return socketPath;
272
+ await new Promise((resolveTimer) => setTimeout(resolveTimer, 100));
223
273
  }
224
- throw new Error(`kobe: daemon did not start at ${socketPath}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
274
+ throw new Error(`kobe: daemon did not start at ${socketPath}: ${lastErr instanceof Error ? lastErr.message : "timeout"}`);
225
275
  }
226
- async function canConnect(client) {
276
+ async function connectOrStartDaemon() {
277
+ const socketPath = await ensureDaemonReachable();
278
+ const client = new KobeDaemonClient(socketPath);
279
+ await client.connect();
280
+ return client;
281
+ }
282
+ async function testCanConnect(socketPath) {
283
+ const probe = new KobeDaemonClient(socketPath);
227
284
  try {
228
- await client.connect();
285
+ await probe.connect();
286
+ probe.close();
229
287
  return true;
230
288
  } catch {
231
- client.close();
289
+ probe.close();
232
290
  return false;
233
291
  }
234
292
  }