@mlx-node/server 0.0.8 → 0.0.10

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 (61) hide show
  1. package/dist/auth.d.ts +56 -0
  2. package/dist/auth.d.ts.map +1 -0
  3. package/dist/auth.js +106 -0
  4. package/dist/chat-session-warm-reuse.d.ts +8 -8
  5. package/dist/chat-session-warm-reuse.d.ts.map +1 -1
  6. package/dist/chat-session-warm-reuse.js +12 -8
  7. package/dist/endpoints/responses.d.ts +3 -1
  8. package/dist/endpoints/responses.d.ts.map +1 -1
  9. package/dist/endpoints/responses.js +38 -5
  10. package/dist/handler.d.ts +27 -1
  11. package/dist/handler.d.ts.map +1 -1
  12. package/dist/handler.js +65 -16
  13. package/dist/health.d.ts +146 -0
  14. package/dist/health.d.ts.map +1 -0
  15. package/dist/health.js +107 -0
  16. package/dist/host/discover.d.ts +19 -0
  17. package/dist/host/discover.d.ts.map +1 -0
  18. package/dist/host/discover.js +50 -0
  19. package/dist/host/env-policy.d.ts +62 -0
  20. package/dist/host/env-policy.d.ts.map +1 -0
  21. package/dist/host/env-policy.js +69 -0
  22. package/dist/host/index.d.ts +202 -0
  23. package/dist/host/index.d.ts.map +1 -0
  24. package/dist/host/index.js +325 -0
  25. package/dist/host/logger.d.ts +36 -0
  26. package/dist/host/logger.d.ts.map +1 -0
  27. package/dist/host/logger.js +376 -0
  28. package/dist/host/net.d.ts +65 -0
  29. package/dist/host/net.d.ts.map +1 -0
  30. package/dist/host/net.js +97 -0
  31. package/dist/host/paths.d.ts +28 -0
  32. package/dist/host/paths.d.ts.map +1 -0
  33. package/dist/host/paths.js +71 -0
  34. package/dist/host/swap.d.ts +27 -0
  35. package/dist/host/swap.d.ts.map +1 -0
  36. package/dist/host/swap.js +178 -0
  37. package/dist/host/temp-root.d.ts +57 -0
  38. package/dist/host/temp-root.d.ts.map +1 -0
  39. package/dist/host/temp-root.js +99 -0
  40. package/dist/index.d.ts +11 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +7 -1
  43. package/dist/load-model.d.ts +69 -0
  44. package/dist/load-model.d.ts.map +1 -0
  45. package/dist/load-model.js +63 -0
  46. package/dist/model-work-coordinator.d.ts +29 -4
  47. package/dist/model-work-coordinator.d.ts.map +1 -1
  48. package/dist/model-work-coordinator.js +97 -16
  49. package/dist/router.d.ts +34 -1
  50. package/dist/router.d.ts.map +1 -1
  51. package/dist/router.js +47 -5
  52. package/dist/server.d.ts +117 -3
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +125 -9
  55. package/dist/session-registry.d.ts +7 -0
  56. package/dist/session-registry.d.ts.map +1 -1
  57. package/dist/session-registry.js +9 -0
  58. package/dist/streaming.d.ts +14 -0
  59. package/dist/streaming.d.ts.map +1 -1
  60. package/dist/streaming.js +45 -0
  61. package/package.json +15 -3
package/dist/server.js CHANGED
@@ -5,11 +5,23 @@ import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { ResponseStore } from '@mlx-node/core';
7
7
  import { createHandler } from './handler.js';
8
+ import { createHealthReporter } from './health.js';
8
9
  import { createIdleSweeper, DEFAULT_IDLE_CLEAR_CACHE_MS, parseIdleClearCacheEnv } from './idle-sweeper.js';
10
+ import { runGuardedModelLoad } from './load-model.js';
9
11
  import { ModelWorkCoordinator } from './model-work-coordinator.js';
10
12
  import { ModelRegistry } from './registry.js';
13
+ import { activeSSEStreamCountForResponses } from './streaming.js';
11
14
  /** Cleanup interval for expired responses (ms). */
12
15
  const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
16
+ /**
17
+ * Default grace period before {@link ServerInstance.close} destroys
18
+ * still-open connections.
19
+ *
20
+ * 5 s is deliberately a little longer than the ~5 s GPU watchdog window: a
21
+ * decode loop that is about to yield its next token should get the chance to
22
+ * unwind cleanly rather than being cut off a hair early.
23
+ */
24
+ const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
13
25
  /**
14
26
  * Default retention for persisted response rows in SQLite, in seconds.
15
27
  *
@@ -84,6 +96,42 @@ function normalizePositiveIntConfig(value, name) {
84
96
  }
85
97
  return value;
86
98
  }
99
+ /**
100
+ * Resolve the effective auth token from an explicit value plus the env
101
+ * fallback.
102
+ *
103
+ * Exported because `createInferenceHost` has to answer "is this server going
104
+ * to be protected?" BEFORE it binds, in order to refuse a non-loopback bind
105
+ * that would serve anonymously. Two independent copies of this rule would
106
+ * drift, and the direction it would drift is a host that refuses to start
107
+ * while `MLX_SERVER_AUTH_TOKEN` is sitting right there in the environment.
108
+ *
109
+ * An empty env var means "not set". An accidental `MLX_SERVER_AUTH_TOKEN=` in
110
+ * a launcher script must not enable auth with an empty secret that every
111
+ * credential-less request would then fail against.
112
+ *
113
+ * An empty EXPLICIT token is a different case and is rejected outright, for the
114
+ * same reason {@link normalizePositiveIntConfig} rejects a bogus explicit knob:
115
+ * somebody asked for a token and supplied nothing, which is
116
+ * `--auth-token "$TOKEN"` with `TOKEN` unset. Returning `''` made the bind
117
+ * guard read "auth is configured" and allow `0.0.0.0`, while the comparator
118
+ * accepted an empty `x-api-key` because both strings were empty — a wildcard
119
+ * bind published under a credential anyone can guess. Quietly downgrading to
120
+ * "no auth" instead would be fail-open in the other direction: on loopback it
121
+ * hands back the unauthenticated, wildcard-CORS server the operator was
122
+ * explicitly trying not to start. Throwing is the only answer that is wrong in
123
+ * neither bind mode, and it happens before anything binds or loads.
124
+ */
125
+ export function resolveAuthToken(explicit) {
126
+ if (explicit === '') {
127
+ throw new Error('authToken was set to an empty string; pass a real secret or omit it entirely ' +
128
+ '(an unset shell variable in `--auth-token "$VAR"` is the usual cause).');
129
+ }
130
+ if (explicit !== undefined)
131
+ return explicit;
132
+ const fromEnv = process.env.MLX_SERVER_AUTH_TOKEN;
133
+ return fromEnv != null && fromEnv !== '' ? fromEnv : undefined;
134
+ }
87
135
  /**
88
136
  * Start an MLX-Node HTTP server exposing `POST /v1/responses`,
89
137
  * `POST /v1/messages`, and `GET /v1/models`.
@@ -97,7 +145,10 @@ function normalizePositiveIntConfig(value, name) {
97
145
  export async function createServer(config) {
98
146
  const port = config?.port ?? 8080;
99
147
  const host = config?.host ?? '127.0.0.1';
100
- const cors = config?.cors ?? true;
148
+ const authToken = resolveAuthToken(config?.authToken);
149
+ // Forwarded unresolved so `createHandler` applies the auth-aware default
150
+ // (`true` without a token, `false` with one) in exactly one place.
151
+ const cors = config?.cors;
101
152
  const disableStore = config?.disableStore ?? false;
102
153
  // Validate caller-supplied numeric knobs BEFORE consulting env fallbacks
103
154
  // so a bogus explicit value surfaces as a descriptive error instead of
@@ -139,6 +190,9 @@ export async function createServer(config) {
139
190
  }
140
191
  }, CLEANUP_INTERVAL_MS);
141
192
  cleanupTimer.unref();
193
+ // One reporter shared by the HTTP endpoint and `ServerInstance.health()`
194
+ // so both report the same uptime origin.
195
+ const health = createHealthReporter({ registry, idleSweeper, modelWorkCoordinator });
142
196
  const handler = createHandler(registry, {
143
197
  cors,
144
198
  store,
@@ -147,8 +201,22 @@ export async function createServer(config) {
147
201
  resolveModel: config?.resolveModel,
148
202
  modelWorkCoordinator,
149
203
  listModels: config?.listModels,
204
+ authToken,
205
+ health,
206
+ });
207
+ /**
208
+ * Responses owned by THIS HTTP server. The SSE registry is deliberately
209
+ * process-wide because `beginSSE` is also used by standalone handlers;
210
+ * shutdown accounting intersects it with this weak ownership set so one
211
+ * server cannot claim streams that another server leaves live. A WeakSet
212
+ * needs no second response cleanup lifecycle.
213
+ */
214
+ const ownedResponses = new WeakSet();
215
+ const server = httpCreateServer((req, res) => {
216
+ // Synchronous, before `handler` can reach either endpoint's `beginSSE`.
217
+ ownedResponses.add(res);
218
+ void handler(req, res);
150
219
  });
151
- const server = httpCreateServer(handler);
152
220
  await new Promise((resolve, reject) => {
153
221
  const onError = (err) => {
154
222
  server.removeListener('error', onError);
@@ -160,21 +228,69 @@ export async function createServer(config) {
160
228
  resolve();
161
229
  });
162
230
  });
163
- return {
164
- server,
165
- registry,
166
- store,
167
- async close() {
231
+ // Memoized so a second `close()` returns the first call's promise instead
232
+ // of re-entering `server.close()` (which invokes its callback with
233
+ // ERR_SERVER_NOT_RUNNING once the server is already down).
234
+ let closePromise = null;
235
+ const close = (opts) => {
236
+ if (closePromise !== null)
237
+ return closePromise;
238
+ const startedAt = Date.now();
239
+ const requested = opts?.timeoutMs;
240
+ const timeoutMs = typeof requested === 'number' && Number.isFinite(requested) && requested >= 0
241
+ ? requested
242
+ : DEFAULT_CLOSE_TIMEOUT_MS;
243
+ closePromise = (async () => {
168
244
  clearInterval(cleanupTimer);
169
245
  idleSweeper.close();
170
- await new Promise((resolve, reject) => {
246
+ let forced = false;
247
+ let streamsAborted = 0;
248
+ // Arm the wait BEFORE dropping idle sockets so nothing can complete in
249
+ // the gap and settle `server.close()` before we are listening.
250
+ const serverClosed = new Promise((resolve, reject) => {
171
251
  server.close((err) => {
172
- if (err)
252
+ // Tolerated defensively: memoization should make this unreachable,
253
+ // but a caller who also closed `instance.server` directly would
254
+ // otherwise turn a successful shutdown into a rejection.
255
+ if (err && err.code !== 'ERR_SERVER_NOT_RUNNING')
173
256
  reject(err);
174
257
  else
175
258
  resolve();
176
259
  });
177
260
  });
261
+ // Keep-alive sockets parked in a client pool hold no request; without
262
+ // this the shutdown would sit on them until the client's own timeout.
263
+ server.closeIdleConnections();
264
+ const forceTimer = setTimeout(() => {
265
+ forced = true;
266
+ // Snapshot BEFORE destroying: `closeAllConnections()` fires each
267
+ // response's `'close'` event, which unregisters it from the global SSE
268
+ // registry used by the intersection.
269
+ streamsAborted = activeSSEStreamCountForResponses(ownedResponses);
270
+ // Destroying the socket fires exactly the `res.on('close')` path a
271
+ // client disconnect fires, so the endpoint's AbortController cancels
272
+ // the native stream handle rather than leaking a running decode.
273
+ server.closeAllConnections();
274
+ }, timeoutMs);
275
+ try {
276
+ await serverClosed;
277
+ }
278
+ finally {
279
+ clearTimeout(forceTimer);
280
+ }
281
+ return { forced, streamsAborted, durationMs: Date.now() - startedAt };
282
+ })();
283
+ return closePromise;
284
+ };
285
+ return {
286
+ server,
287
+ registry,
288
+ store,
289
+ modelWork: modelWorkCoordinator,
290
+ health,
291
+ close,
292
+ loadModel(opts) {
293
+ return runGuardedModelLoad({ idleSweeper, modelWorkCoordinator, registry }, opts);
178
294
  },
179
295
  withSuspendedDrains(fn) {
180
296
  return idleSweeper.withSuspendedDrains(fn);
@@ -281,6 +281,13 @@ export declare class SessionRegistry {
281
281
  * `fn`. Primarily for tests and diagnostics.
282
282
  */
283
283
  get queueDepth(): number;
284
+ /**
285
+ * Configured waiter cap for this model's execution mutex, or `undefined`
286
+ * when unbounded. Paired with {@link queueDepth} so a readiness probe can
287
+ * tell "3 waiters, unbounded" (fine) from "3 waiters, cap of 3" (the next
288
+ * request gets a 429) without reaching into private state.
289
+ */
290
+ get queueDepthLimit(): number | undefined;
284
291
  /**
285
292
  * Current sampling defaults applied to every new `ChatSession` this
286
293
  * registry allocates. Exposed primarily for tests and diagnostics.
@@ -1 +1 @@
1
- {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4FG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAyErE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kCAAkC,IAAI,IAAI,CAGzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAE3D;AAuDD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,CA2BzF;AAED,uDAAuD;AACvD,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,KAAK,EAAE,mBAAmB,CAAC;IAC3B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;OAYG;IACH,gBAAgB,CAAC,EAAE,UAAU,CAAC;IAC9B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB,YAAY,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAK7C;CACF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,WAAW,CAAC,mBAAmB,CAAC,CAAC;IAC1C,GAAG,EAAE,OAAO,CAAC;CACd;AAsCD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,eAAe,CAAqB;IAC5C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,WAAW,CAAK;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwC;IAChE;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;IAChE;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,QAAQ,CAAmC;IAEnD,YAAY,IAAI,EAAE,sBAAsB,EAMvC;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IASlB;;;;OAIG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;;OAGG;IACH,IAAI,qBAAqB,IAAI,UAAU,GAAG,SAAS,CAElD;IAED,IAAI,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAEzC;IAED;;;;;;;;OAQG;IACH,mBAAmB,CAAC,QAAQ,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,CAE1D;IAED,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAElD;IAED,+FAA+F;IAC/F,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkDG;IACH,WAAW,CACT,kBAAkB,EAAE,MAAM,GAAG,IAAI,EACjC,qBAAqB,EAAE,MAAM,GAAG,IAAI,EACpC,cAAc,GAAE,MAAM,GAAG,IAAW,GACnC,mBAAmB,CAsErB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,kBAAkB,IAAI,mBAAmB,CAExC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8EG;IACH,kBAAkB,CAAC,qBAAqB,EAAE,MAAM,GAAG,IAAI,GAAG,mBAAmB,CAiB5E;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CACH,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,WAAW,CAAC,mBAAmB,CAAC,EACzC,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,cAAc,GAAE,MAAM,GAAG,IAAI,GAAG,SAAgB,GAC/C,IAAI,CAcN;IAED;;OAEG;IACH,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAE7B;IAED;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAOZ;IAED,2DAA2D;IAC3D,KAAK,IAAI,IAAI,CAEZ;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACH,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA4BjD;IAED;;;;;;;;;;OAUG;YACW,aAAa;CAiD5B"}
1
+ {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"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"}
@@ -406,6 +406,15 @@ export class SessionRegistry {
406
406
  get queueDepth() {
407
407
  return this.queuedCount;
408
408
  }
409
+ /**
410
+ * Configured waiter cap for this model's execution mutex, or `undefined`
411
+ * when unbounded. Paired with {@link queueDepth} so a readiness probe can
412
+ * tell "3 waiters, unbounded" (fine) from "3 waiters, cap of 3" (the next
413
+ * request gets a 429) without reaching into private state.
414
+ */
415
+ get queueDepthLimit() {
416
+ return this.maxQueueDepth;
417
+ }
409
418
  /**
410
419
  * Current sampling defaults applied to every new `ChatSession` this
411
420
  * registry allocates. Exposed primarily for tests and diagnostics.
@@ -4,4 +4,18 @@ export declare function beginSSE(res: ServerResponse): void;
4
4
  /** Write one SSE event. Injects `type: eventType` into the payload (data's own `type` wins) for OpenAI SDK compatibility. */
5
5
  export declare function writeSSEEvent(res: ServerResponse, eventType: string, data: object): void;
6
6
  export declare function endSSE(res: ServerResponse): void;
7
+ /**
8
+ * Number of SSE streams currently open process-wide. Diagnostics and
9
+ * shutdown accounting only.
10
+ */
11
+ export declare function activeSSEStreamCount(): number;
12
+ /**
13
+ * Number of active SSE streams among a caller-owned collection of responses.
14
+ *
15
+ * `createServer()` uses this to intersect the process-wide SSE registry with
16
+ * the responses accepted by one `node:http` Server. Keep the no-argument
17
+ * {@link activeSSEStreamCount} above for process-wide diagnostics and
18
+ * standalone `createHandler()` consumers.
19
+ */
20
+ export declare function activeSSEStreamCountForResponses(responses: WeakSet<ServerResponse>): number;
7
21
  //# sourceMappingURL=streaming.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAMlD;AAED,6HAA6H;AAC7H,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAGxF;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAEhD"}
1
+ {"version":3,"file":"streaming.d.ts","sourceRoot":"","sources":["../src/streaming.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAgBhD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAclD;AAED,6HAA6H;AAC7H,wBAAgB,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAGxF;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,cAAc,GAAG,IAAI,CAGhD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,MAAM,CAM3F"}
package/dist/streaming.js CHANGED
@@ -1,5 +1,26 @@
1
1
  /** SSE writer utilities. */
2
+ /**
3
+ * Responses that have committed to SSE (`beginSSE`) but have not yet been
4
+ * ended (`endSSE`) or had their connection torn down.
5
+ *
6
+ * Purely an accounting aid for {@link activeSSEStreamCount}, which graceful
7
+ * shutdown reads to report how many streams a forced close cut short. Nothing
8
+ * in the request path branches on membership, so a miscount can only skew a
9
+ * diagnostic number — never the behaviour of a live stream.
10
+ *
11
+ * Module-scoped because `@mlx-node/server` is loaded once per process and
12
+ * `beginSSE`/`endSSE` are free functions called from both endpoints.
13
+ */
14
+ const activeSSEResponses = new Set();
2
15
  export function beginSSE(res) {
16
+ activeSSEResponses.add(res);
17
+ // Belt-and-braces cleanup: a stream torn down by a client disconnect (or by
18
+ // `server.closeAllConnections()`) may unwind through an error path that
19
+ // never reaches `endSSE`. Without this the entry would leak for the life of
20
+ // the process and inflate the count forever.
21
+ res.once('close', () => {
22
+ activeSSEResponses.delete(res);
23
+ });
3
24
  res.writeHead(200, {
4
25
  'Content-Type': 'text/event-stream',
5
26
  'Cache-Control': 'no-cache',
@@ -12,5 +33,29 @@ export function writeSSEEvent(res, eventType, data) {
12
33
  res.write(`event: ${eventType}\ndata: ${JSON.stringify(payload)}\n\n`);
13
34
  }
14
35
  export function endSSE(res) {
36
+ activeSSEResponses.delete(res);
15
37
  res.end();
16
38
  }
39
+ /**
40
+ * Number of SSE streams currently open process-wide. Diagnostics and
41
+ * shutdown accounting only.
42
+ */
43
+ export function activeSSEStreamCount() {
44
+ return activeSSEResponses.size;
45
+ }
46
+ /**
47
+ * Number of active SSE streams among a caller-owned collection of responses.
48
+ *
49
+ * `createServer()` uses this to intersect the process-wide SSE registry with
50
+ * the responses accepted by one `node:http` Server. Keep the no-argument
51
+ * {@link activeSSEStreamCount} above for process-wide diagnostics and
52
+ * standalone `createHandler()` consumers.
53
+ */
54
+ export function activeSSEStreamCountForResponses(responses) {
55
+ let count = 0;
56
+ for (const response of activeSSEResponses) {
57
+ if (responses.has(response))
58
+ count += 1;
59
+ }
60
+ return count;
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlx-node/server",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "homepage": "https://github.com/mlx-node/mlx-node",
5
5
  "bugs": {
6
6
  "url": "https://github.com/mlx-node/mlx-node/issues"
@@ -21,14 +21,26 @@
21
21
  ".": {
22
22
  "types": "./dist/index.d.ts",
23
23
  "import": "./dist/index.js"
24
+ },
25
+ "./host": {
26
+ "types": "./dist/host/index.d.ts",
27
+ "import": "./dist/host/index.js"
28
+ },
29
+ "./host/env-policy": {
30
+ "types": "./dist/host/env-policy.d.ts",
31
+ "import": "./dist/host/env-policy.js"
32
+ },
33
+ "./host/paths": {
34
+ "types": "./dist/host/paths.d.ts",
35
+ "import": "./dist/host/paths.js"
24
36
  }
25
37
  },
26
38
  "scripts": {
27
39
  "build": "tsc -b"
28
40
  },
29
41
  "dependencies": {
30
- "@mlx-node/core": "0.0.8",
31
- "@mlx-node/lm": "0.0.8"
42
+ "@mlx-node/core": "0.0.10",
43
+ "@mlx-node/lm": "0.0.10"
32
44
  },
33
45
  "devDependencies": {
34
46
  "@types/node": "@types/node@24.12.2"