@mlx-node/server 0.0.8 → 0.0.9
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/auth.d.ts +56 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +106 -0
- package/dist/chat-session-warm-reuse.d.ts +8 -8
- package/dist/chat-session-warm-reuse.d.ts.map +1 -1
- package/dist/chat-session-warm-reuse.js +12 -8
- package/dist/endpoints/responses.d.ts +3 -1
- package/dist/endpoints/responses.d.ts.map +1 -1
- package/dist/endpoints/responses.js +38 -5
- package/dist/handler.d.ts +27 -1
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +65 -16
- package/dist/health.d.ts +146 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +107 -0
- package/dist/host/discover.d.ts +19 -0
- package/dist/host/discover.d.ts.map +1 -0
- package/dist/host/discover.js +50 -0
- package/dist/host/env-policy.d.ts +62 -0
- package/dist/host/env-policy.d.ts.map +1 -0
- package/dist/host/env-policy.js +69 -0
- package/dist/host/index.d.ts +202 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +325 -0
- package/dist/host/logger.d.ts +36 -0
- package/dist/host/logger.d.ts.map +1 -0
- package/dist/host/logger.js +376 -0
- package/dist/host/net.d.ts +65 -0
- package/dist/host/net.d.ts.map +1 -0
- package/dist/host/net.js +97 -0
- package/dist/host/paths.d.ts +28 -0
- package/dist/host/paths.d.ts.map +1 -0
- package/dist/host/paths.js +71 -0
- package/dist/host/swap.d.ts +27 -0
- package/dist/host/swap.d.ts.map +1 -0
- package/dist/host/swap.js +178 -0
- package/dist/host/temp-root.d.ts +57 -0
- package/dist/host/temp-root.d.ts.map +1 -0
- package/dist/host/temp-root.js +99 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/load-model.d.ts +69 -0
- package/dist/load-model.d.ts.map +1 -0
- package/dist/load-model.js +63 -0
- package/dist/model-work-coordinator.d.ts +29 -4
- package/dist/model-work-coordinator.d.ts.map +1 -1
- package/dist/model-work-coordinator.js +97 -16
- package/dist/router.d.ts +34 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -5
- package/dist/server.d.ts +117 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +125 -9
- package/dist/session-registry.d.ts +7 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +9 -0
- package/dist/streaming.d.ts +14 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +45 -0
- package/package.json +15 -3
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-resident lazy-load policy for an inference host.
|
|
3
|
+
*
|
|
4
|
+
* The host discovers every local model up-front but loads at most one into
|
|
5
|
+
* the `ModelRegistry` at a time. Switching models (e.g. via Claude Code's
|
|
6
|
+
* `/model` picker, or the desktop app's model menu) unregisters the previous
|
|
7
|
+
* instance, letting GC + native destructors reclaim memory, before loading
|
|
8
|
+
* the new one.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Build the `resolveModel` + `listModels` callbacks for the handler.
|
|
12
|
+
*
|
|
13
|
+
* `loadModelFn` is injected so tests can stub it without touching native code.
|
|
14
|
+
* The controller serializes every `resolveModel` invocation on a single
|
|
15
|
+
* promise chain so two concurrent requests for different-but-currently-
|
|
16
|
+
* unloaded models cannot race on the native compiled-path globals.
|
|
17
|
+
*/
|
|
18
|
+
export function makeSwapController(discovered, registry, loadModelFn, defaultName) {
|
|
19
|
+
const byName = new Map();
|
|
20
|
+
for (const entry of discovered)
|
|
21
|
+
byName.set(entry.name, entry);
|
|
22
|
+
const ordered = [...discovered].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
23
|
+
// Which entry unknown names (haiku subagent dispatches, etc.) fall back
|
|
24
|
+
// to before anything is resident. Defaults to discovered[0], but the
|
|
25
|
+
// caller can pin it to the user's `--model` pick so the first haiku
|
|
26
|
+
// title-gen doesn't trigger a load of the alphabetically-first model
|
|
27
|
+
// followed by an immediate swap to the user's real choice.
|
|
28
|
+
const fallbackEntry = (defaultName != null ? byName.get(defaultName) : undefined) ?? discovered[0];
|
|
29
|
+
let resident = null;
|
|
30
|
+
// Names we registered as aliases to the current resident (e.g.
|
|
31
|
+
// Claude Code's hardcoded `claude-haiku-*` for subagent dispatches /
|
|
32
|
+
// title generation). Tracked so we can unregister them on `/model`
|
|
33
|
+
// swap — otherwise an alias's refcount would keep the old binding
|
|
34
|
+
// alive past the user's swap.
|
|
35
|
+
const aliases = new Set();
|
|
36
|
+
let currentOp = Promise.resolve();
|
|
37
|
+
async function resolveModel(name) {
|
|
38
|
+
// Fast path: already registered under this name (either as a real
|
|
39
|
+
// resident or as an alias we previously installed). Avoid chaining.
|
|
40
|
+
if (registry.get(name))
|
|
41
|
+
return;
|
|
42
|
+
const next = currentOp.then(async () => {
|
|
43
|
+
// Re-check under the serialized section — a prior waiter may have loaded it.
|
|
44
|
+
if (registry.get(name))
|
|
45
|
+
return;
|
|
46
|
+
// Pick the discovered entry to resolve against. If the requested
|
|
47
|
+
// name matches a discovered model, use it. Otherwise (unknown
|
|
48
|
+
// name — Claude Code's hardcoded small-fast-model, etc.) fall
|
|
49
|
+
// through to the current resident so subagent dispatches don't
|
|
50
|
+
// 404, loading discovered[0] on first boot if nothing is resident
|
|
51
|
+
// yet.
|
|
52
|
+
//
|
|
53
|
+
// CRITICAL: this must read `resident` at RUN time, not QUEUE time.
|
|
54
|
+
// If we capture it before chaining onto `currentOp`, a swap that
|
|
55
|
+
// ran ahead of us will leave us with a stale target — e.g. a haiku
|
|
56
|
+
// alias request that arrived during a `/model a → b` switch would
|
|
57
|
+
// capture `targetEntry = a`, then re-bind itself to `a` and undo
|
|
58
|
+
// the user's switch when its turn finally comes around.
|
|
59
|
+
const knownEntry = byName.get(name);
|
|
60
|
+
const targetEntry = knownEntry ?? (resident ? (byName.get(resident.name) ?? fallbackEntry) : fallbackEntry);
|
|
61
|
+
const isAlias = targetEntry.name !== name;
|
|
62
|
+
// Swap out any stale resident that isn't the target.
|
|
63
|
+
//
|
|
64
|
+
// We do NOT unregister the aliases here: in-flight messages.ts
|
|
65
|
+
// requests may be microtask-racing between "resolveModel returned"
|
|
66
|
+
// and "registry.get(body.model)" and dropping the alias in that
|
|
67
|
+
// window yields a spurious 404. Instead we carry the alias set
|
|
68
|
+
// across the swap and re-point them to the new resident below,
|
|
69
|
+
// so the name always resolves to *some* live instance.
|
|
70
|
+
const oldResident = resident;
|
|
71
|
+
const carriedAliases = new Set(aliases);
|
|
72
|
+
if (oldResident && oldResident.name !== targetEntry.name) {
|
|
73
|
+
// Drop our local alias bookkeeping AND the old resident's primary
|
|
74
|
+
// name binding, but leave the alias *names* in the registry pointed
|
|
75
|
+
// at the old model — they hold the only refcount preventing GC,
|
|
76
|
+
// and an in-flight `registry.get(alias)` must keep resolving to
|
|
77
|
+
// *some* live instance until the new model is in hand.
|
|
78
|
+
aliases.clear();
|
|
79
|
+
registry.unregister(oldResident.name);
|
|
80
|
+
resident = null;
|
|
81
|
+
}
|
|
82
|
+
// Ensure the target is resident. If the load throws, restore the
|
|
83
|
+
// pre-swap controller state so future swap attempts know about the
|
|
84
|
+
// aliases we just cleared — otherwise the alias *names* stay bound
|
|
85
|
+
// in the registry to the old model object forever (alias bindings
|
|
86
|
+
// hold their own refcount), but the controller forgets they exist
|
|
87
|
+
// and never repoints them, leaving alias-routed traffic permanently
|
|
88
|
+
// pinned to a stale model.
|
|
89
|
+
let instance = registry.get(targetEntry.name);
|
|
90
|
+
if (!instance) {
|
|
91
|
+
let loaded;
|
|
92
|
+
try {
|
|
93
|
+
loaded = await loadModelFn(targetEntry.path);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// Recovery: re-populate the controller's alias set so the next
|
|
97
|
+
// resolveModel call still owns them. The alias→old-model bindings
|
|
98
|
+
// are still live in the registry (we never unregistered them), so
|
|
99
|
+
// we can recover the old model object via any surviving alias and
|
|
100
|
+
// re-bind the old resident's primary name.
|
|
101
|
+
//
|
|
102
|
+
// If `carriedAliases` is empty (no aliases ever existed) AND we
|
|
103
|
+
// unregistered `oldResident.name`, the binding's refcount may have
|
|
104
|
+
// hit zero and the model is gone. There's nothing the controller
|
|
105
|
+
// can do to recover in that case — the user will need to /model-
|
|
106
|
+
// pick again. This is acceptable: alias-less load failures are
|
|
107
|
+
// rare, and the user-facing symptom is "prior model gone, please
|
|
108
|
+
// re-pick", not silently-wrong responses.
|
|
109
|
+
for (const aliasName of carriedAliases)
|
|
110
|
+
aliases.add(aliasName);
|
|
111
|
+
if (oldResident) {
|
|
112
|
+
let oldInstance;
|
|
113
|
+
for (const aliasName of carriedAliases) {
|
|
114
|
+
const probe = registry.get(aliasName);
|
|
115
|
+
if (probe) {
|
|
116
|
+
oldInstance = probe;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (oldInstance) {
|
|
121
|
+
const oldEntry = byName.get(oldResident.name);
|
|
122
|
+
registry.register(oldResident.name, oldInstance, {
|
|
123
|
+
samplingDefaults: oldEntry?.preset.sampling,
|
|
124
|
+
maxOutputTokens: oldEntry?.preset.maxOutputTokens,
|
|
125
|
+
});
|
|
126
|
+
resident = oldResident;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
instance = loaded;
|
|
132
|
+
registry.register(targetEntry.name, instance, {
|
|
133
|
+
samplingDefaults: targetEntry.preset.sampling,
|
|
134
|
+
maxOutputTokens: targetEntry.preset.maxOutputTokens,
|
|
135
|
+
});
|
|
136
|
+
resident = { name: targetEntry.name };
|
|
137
|
+
}
|
|
138
|
+
else if (!resident) {
|
|
139
|
+
resident = { name: targetEntry.name };
|
|
140
|
+
}
|
|
141
|
+
// Re-point any aliases carried across the swap onto the new
|
|
142
|
+
// resident. `registry.register(sameName, differentModel)` drops
|
|
143
|
+
// the old binding's refcount and installs the new one atomically,
|
|
144
|
+
// so any concurrent `registry.get(alias)` either sees the old or
|
|
145
|
+
// new instance — never null.
|
|
146
|
+
for (const aliasName of carriedAliases) {
|
|
147
|
+
if (aliasName === targetEntry.name)
|
|
148
|
+
continue;
|
|
149
|
+
registry.register(aliasName, instance, {
|
|
150
|
+
samplingDefaults: targetEntry.preset.sampling,
|
|
151
|
+
maxOutputTokens: targetEntry.preset.maxOutputTokens,
|
|
152
|
+
});
|
|
153
|
+
aliases.add(aliasName);
|
|
154
|
+
}
|
|
155
|
+
// For unknown names, register an alias on the resident instance so
|
|
156
|
+
// the endpoint's `registry.get(name)` lookup succeeds.
|
|
157
|
+
if (isAlias) {
|
|
158
|
+
registry.register(name, instance, {
|
|
159
|
+
samplingDefaults: targetEntry.preset.sampling,
|
|
160
|
+
maxOutputTokens: targetEntry.preset.maxOutputTokens,
|
|
161
|
+
});
|
|
162
|
+
aliases.add(name);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
currentOp = next.catch(() => undefined);
|
|
166
|
+
await next;
|
|
167
|
+
}
|
|
168
|
+
function listModels() {
|
|
169
|
+
const created = Math.floor(Date.now() / 1000);
|
|
170
|
+
return ordered.map((entry) => ({
|
|
171
|
+
id: entry.name,
|
|
172
|
+
object: 'model',
|
|
173
|
+
created,
|
|
174
|
+
owned_by: 'mlx-node',
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
return { resolveModel, listModels };
|
|
178
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pid-scoped temp roots for the host's paged-config overrides, plus the
|
|
3
|
+
* startup sweep that reclaims the ones a killed host left behind.
|
|
4
|
+
*
|
|
5
|
+
* `PagedConfigOverrideManager` clones a checkpoint directory (config.json
|
|
6
|
+
* rewritten, everything else symlinked) into a temp root and removes that root
|
|
7
|
+
* in `cleanup()`. `cleanup()` runs on a normal shutdown — but the host's
|
|
8
|
+
* headline deployment is an Electron `utilityProcess`, and a `utilityProcess`
|
|
9
|
+
* can be SIGKILLed (app force-quit, OOM killer, `kill -9`). SIGKILL runs no
|
|
10
|
+
* handler, so without a sweep EVERY hard kill leaks a root. The clones are
|
|
11
|
+
* symlink farms rather than copies, so the leak is inodes and directory
|
|
12
|
+
* entries rather than model-sized bytes — but the roots accumulate forever and
|
|
13
|
+
* a partially-written clone can hold a real config.json.
|
|
14
|
+
*
|
|
15
|
+
* The reclaim strategy is "name the owner in the directory name": the manager
|
|
16
|
+
* gets a `tempDirPrefix` of `mlx-inference-host-<pid>-`, `mkdtemp` appends its
|
|
17
|
+
* own random suffix, and {@link sweepOrphanHostTempRoots} parses the pid back
|
|
18
|
+
* out and removes any root whose owner is gone.
|
|
19
|
+
*
|
|
20
|
+
* Known limitation: pid reuse. A long-dead host's root whose pid has since
|
|
21
|
+
* been recycled by an unrelated process is SPARED (never wrongly deleted), so
|
|
22
|
+
* the failure mode is a leaked directory, not data loss.
|
|
23
|
+
*/
|
|
24
|
+
/** Shared stem. A directory is host-owned iff its name starts with this. */
|
|
25
|
+
export declare const HOST_TEMP_DIR_STEM = "mlx-inference-host-";
|
|
26
|
+
/**
|
|
27
|
+
* `tempDirPrefix` to hand `PagedConfigOverrideManager` so the root it creates
|
|
28
|
+
* carries its owner's pid.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hostTempDirPrefix(pid?: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* Does a process with this pid exist?
|
|
33
|
+
*
|
|
34
|
+
* `process.kill(pid, 0)` sends no signal; it only performs the permission +
|
|
35
|
+
* existence check. `ESRCH` is the sole "gone" answer — `EPERM` means the
|
|
36
|
+
* process exists but belongs to another user, which must count as ALIVE so a
|
|
37
|
+
* multi-user box never has one user's sweep delete another user's live root.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isProcessAlive(pid: number): boolean;
|
|
40
|
+
export interface SweepOrphanTempRootsOptions {
|
|
41
|
+
/** Directory to scan. Defaults to the OS temp dir. */
|
|
42
|
+
root?: string;
|
|
43
|
+
/** Our own pid — never swept, however the liveness probe answers. */
|
|
44
|
+
selfPid?: number;
|
|
45
|
+
/** Injectable liveness probe. Defaults to {@link isProcessAlive}. */
|
|
46
|
+
isAlive?: (pid: number) => boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove every host temp root whose owning pid is no longer alive.
|
|
50
|
+
*
|
|
51
|
+
* Best effort by contract: a scan or unlink failure (permissions, a root a
|
|
52
|
+
* concurrently-exiting host is removing under us) is swallowed, because a
|
|
53
|
+
* housekeeping step must never be the reason a host refuses to start.
|
|
54
|
+
* Returns the absolute paths actually removed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sweepOrphanHostTempRoots(opts?: SweepOrphanTempRootsOptions): Promise<string[]>;
|
|
57
|
+
//# sourceMappingURL=temp-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-root.d.ts","sourceRoot":"","sources":["../../src/host/temp-root.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AAKxD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAoB,GAAG,MAAM,CAEnE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWnD;AAED,MAAM,WAAW,2BAA2B;IAC1C,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,GAAE,2BAAgC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BxG"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pid-scoped temp roots for the host's paged-config overrides, plus the
|
|
3
|
+
* startup sweep that reclaims the ones a killed host left behind.
|
|
4
|
+
*
|
|
5
|
+
* `PagedConfigOverrideManager` clones a checkpoint directory (config.json
|
|
6
|
+
* rewritten, everything else symlinked) into a temp root and removes that root
|
|
7
|
+
* in `cleanup()`. `cleanup()` runs on a normal shutdown — but the host's
|
|
8
|
+
* headline deployment is an Electron `utilityProcess`, and a `utilityProcess`
|
|
9
|
+
* can be SIGKILLed (app force-quit, OOM killer, `kill -9`). SIGKILL runs no
|
|
10
|
+
* handler, so without a sweep EVERY hard kill leaks a root. The clones are
|
|
11
|
+
* symlink farms rather than copies, so the leak is inodes and directory
|
|
12
|
+
* entries rather than model-sized bytes — but the roots accumulate forever and
|
|
13
|
+
* a partially-written clone can hold a real config.json.
|
|
14
|
+
*
|
|
15
|
+
* The reclaim strategy is "name the owner in the directory name": the manager
|
|
16
|
+
* gets a `tempDirPrefix` of `mlx-inference-host-<pid>-`, `mkdtemp` appends its
|
|
17
|
+
* own random suffix, and {@link sweepOrphanHostTempRoots} parses the pid back
|
|
18
|
+
* out and removes any root whose owner is gone.
|
|
19
|
+
*
|
|
20
|
+
* Known limitation: pid reuse. A long-dead host's root whose pid has since
|
|
21
|
+
* been recycled by an unrelated process is SPARED (never wrongly deleted), so
|
|
22
|
+
* the failure mode is a leaked directory, not data loss.
|
|
23
|
+
*/
|
|
24
|
+
import { readdir, rm } from 'node:fs/promises';
|
|
25
|
+
import { tmpdir } from 'node:os';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
/** Shared stem. A directory is host-owned iff its name starts with this. */
|
|
28
|
+
export const HOST_TEMP_DIR_STEM = 'mlx-inference-host-';
|
|
29
|
+
/** Matches `mlx-inference-host-<pid>-<mkdtemp suffix>`. */
|
|
30
|
+
const HOST_TEMP_DIR_RE = /^mlx-inference-host-(\d+)-/;
|
|
31
|
+
/**
|
|
32
|
+
* `tempDirPrefix` to hand `PagedConfigOverrideManager` so the root it creates
|
|
33
|
+
* carries its owner's pid.
|
|
34
|
+
*/
|
|
35
|
+
export function hostTempDirPrefix(pid = process.pid) {
|
|
36
|
+
return `${HOST_TEMP_DIR_STEM}${pid}-`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Does a process with this pid exist?
|
|
40
|
+
*
|
|
41
|
+
* `process.kill(pid, 0)` sends no signal; it only performs the permission +
|
|
42
|
+
* existence check. `ESRCH` is the sole "gone" answer — `EPERM` means the
|
|
43
|
+
* process exists but belongs to another user, which must count as ALIVE so a
|
|
44
|
+
* multi-user box never has one user's sweep delete another user's live root.
|
|
45
|
+
*/
|
|
46
|
+
export function isProcessAlive(pid) {
|
|
47
|
+
// pid 0 addresses the caller's whole process group on POSIX and pid < 0 a
|
|
48
|
+
// group by id; neither is ever a real owner, and signalling them would be
|
|
49
|
+
// actively dangerous. Treat as alive so they are never swept.
|
|
50
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
51
|
+
return true;
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return err.code !== 'ESRCH';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Remove every host temp root whose owning pid is no longer alive.
|
|
62
|
+
*
|
|
63
|
+
* Best effort by contract: a scan or unlink failure (permissions, a root a
|
|
64
|
+
* concurrently-exiting host is removing under us) is swallowed, because a
|
|
65
|
+
* housekeeping step must never be the reason a host refuses to start.
|
|
66
|
+
* Returns the absolute paths actually removed.
|
|
67
|
+
*/
|
|
68
|
+
export async function sweepOrphanHostTempRoots(opts = {}) {
|
|
69
|
+
const root = opts.root ?? tmpdir();
|
|
70
|
+
const selfPid = opts.selfPid ?? process.pid;
|
|
71
|
+
const isAlive = opts.isAlive ?? isProcessAlive;
|
|
72
|
+
let entries;
|
|
73
|
+
try {
|
|
74
|
+
entries = await readdir(root);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
const removed = [];
|
|
80
|
+
for (const name of entries) {
|
|
81
|
+
const match = HOST_TEMP_DIR_RE.exec(name);
|
|
82
|
+
if (match === null)
|
|
83
|
+
continue;
|
|
84
|
+
const pid = Number.parseInt(match[1], 10);
|
|
85
|
+
if (pid === selfPid)
|
|
86
|
+
continue;
|
|
87
|
+
if (isAlive(pid))
|
|
88
|
+
continue;
|
|
89
|
+
const full = join(root, name);
|
|
90
|
+
try {
|
|
91
|
+
await rm(full, { recursive: true, force: true });
|
|
92
|
+
removed.push(full);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* another sweeper won the race, or we lack permission; leave it */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return removed;
|
|
99
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
* `GET /v1/models`, in both streaming (SSE) and non-streaming modes.
|
|
6
6
|
*/
|
|
7
7
|
export { createServer } from './server.js';
|
|
8
|
-
export type { ServerConfig, ServerInstance } from './server.js';
|
|
8
|
+
export type { CloseOptions, CloseResult, ServerConfig, ServerInstance } from './server.js';
|
|
9
|
+
/**
|
|
10
|
+
* Readiness reporting. `deriveHealthStatus` is pure — a supervisor can reuse
|
|
11
|
+
* it to classify a `ServerHealth` body fetched over HTTP.
|
|
12
|
+
*/
|
|
13
|
+
export { createHealthReporter, deriveHealthStatus, toMinimalHealth } from './health.js';
|
|
14
|
+
export type { HealthReporterDeps, HealthStatusInputs, ModelLoadRecord, ServerHealth, ServerHealthMinimal, ServerHealthStatus, } from './health.js';
|
|
15
|
+
export { ModelWorkCoordinator } from './model-work-coordinator.js';
|
|
16
|
+
export type { ModelLoadOutcome } from './model-work-coordinator.js';
|
|
17
|
+
export type { GuardedLoadDeps, LoadModelOptions } from './load-model.js';
|
|
9
18
|
/**
|
|
10
19
|
* Internal helpers re-exported for unit testing only. Not part of the
|
|
11
20
|
* supported public API — names may change without notice.
|
|
@@ -25,5 +34,5 @@ export type { LaunchPreset } from './presets.js';
|
|
|
25
34
|
export type { PublicModelEntry } from './handler.js';
|
|
26
35
|
export type { ResponsesAPIRequest, ResponseObject, ResponseUsage, ResponseError, InputItem, InputMessage, InputFunctionCall, InputFunctionCallOutput, OutputItem, MessageOutputItem, ReasoningOutputItem, FunctionCallOutputItem, OutputTextPart, SummaryTextPart, ResponsesToolDefinition, ContentPart, InputTextPart, StreamEvent, } from './types.js';
|
|
27
36
|
export type { AnthropicCountTokensRequest, AnthropicCountTokensResponse, AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicMessage, AnthropicContentBlock, AnthropicTextContentBlock, AnthropicImageContentBlock, AnthropicToolResultContentBlock, AnthropicToolUseContentBlock, AnthropicThinkingContentBlock, AnthropicToolDefinition, AnthropicToolChoice, AnthropicResponseContent, AnthropicResponseTextBlock, AnthropicResponseThinkingBlock, AnthropicResponseToolUseBlock, AnthropicUsage, AnthropicStreamEvent, AnthropicMessageStartEvent, AnthropicContentBlockStartEvent, AnthropicContentBlockDeltaEvent, AnthropicContentBlockStopEvent, AnthropicMessageDeltaEvent, AnthropicMessageStopEvent, AnthropicDelta, AnthropicTextDelta, AnthropicThinkingDelta, AnthropicInputJsonDelta, SystemBlock, } from './types-anthropic.js';
|
|
28
|
-
export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
|
|
37
|
+
export { writeSSEEvent, beginSSE, endSSE, activeSSEStreamCount } from './streaming.js';
|
|
29
38
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3F;;;GAGG;AACH,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACxF,YAAY,EACV,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AACpE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEzE;;;GAGG;AACH,OAAO,EAAE,eAAe,IAAI,iBAAiB,EAAE,mBAAmB,IAAI,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,EACL,iBAAiB,IAAI,mBAAmB,EACxC,sBAAsB,IAAI,wBAAwB,EAClD,2BAA2B,IAAI,6BAA6B,GAC7D,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEtG,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxE,YAAY,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AACzF,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAS1D,OAAO,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACxH,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,YAAY,EACV,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,EAC1B,+BAA+B,EAC/B,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,8BAA8B,EAC9B,6BAA6B,EAC7B,cAAc,EACd,oBAAoB,EACpB,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,0BAA0B,EAC1B,yBAAyB,EACzB,cAAc,EACd,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,EACvB,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* `GET /v1/models`, in both streaming (SSE) and non-streaming modes.
|
|
6
6
|
*/
|
|
7
7
|
export { createServer } from './server.js';
|
|
8
|
+
/**
|
|
9
|
+
* Readiness reporting. `deriveHealthStatus` is pure — a supervisor can reuse
|
|
10
|
+
* it to classify a `ServerHealth` body fetched over HTTP.
|
|
11
|
+
*/
|
|
12
|
+
export { createHealthReporter, deriveHealthStatus, toMinimalHealth } from './health.js';
|
|
13
|
+
export { ModelWorkCoordinator } from './model-work-coordinator.js';
|
|
8
14
|
/**
|
|
9
15
|
* Internal helpers re-exported for unit testing only. Not part of the
|
|
10
16
|
* supported public API — names may change without notice.
|
|
@@ -23,4 +29,4 @@ export { resolveServerTuningForUsage } from './timing.js';
|
|
|
23
29
|
// import it from the deep path
|
|
24
30
|
// `packages/server/src/session-registry.js` instead.
|
|
25
31
|
export { QWEN_SAMPLING_DEFAULTS, GEMMA4_SAMPLING_DEFAULTS, LFM2_SAMPLING_DEFAULTS, LAUNCH_PRESETS } from './presets.js';
|
|
26
|
-
export { writeSSEEvent, beginSSE, endSSE } from './streaming.js';
|
|
32
|
+
export { writeSSEEvent, beginSSE, endSSE, activeSSEStreamCount } from './streaming.js';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe out-of-band model load.
|
|
3
|
+
*
|
|
4
|
+
* A supervisor (or a `/model`-picker UI) needs to swap the resident model on a
|
|
5
|
+
* server that is already serving. Doing that naively races the Metal
|
|
6
|
+
* allocator — a corruption/abort bug, not a latency bug — so the bracketing
|
|
7
|
+
* order below is load-bearing:
|
|
8
|
+
*
|
|
9
|
+
* ```text
|
|
10
|
+
* idleSweeper.withSuspendedDrains( ← OUTSIDE
|
|
11
|
+
* modelWorkCoordinator.withModelLoad( ← INSIDE
|
|
12
|
+
* load() ; registry.register(...)
|
|
13
|
+
* , label))
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Why the suspension must be OUTSIDE the writer lock: `endRequest()` arms a
|
|
17
|
+
* drain timer at `t + idleClearCacheMs` whose callback runs
|
|
18
|
+
* `__internal__.clearCache()`, walking the process-wide MLX free pool. A load
|
|
19
|
+
* that arrives while another load holds the writer slot parks inside
|
|
20
|
+
* `acquireWrite()` — and if the suspension were taken only after the lock was
|
|
21
|
+
* won, that armed timer would still be live during the wait AND during the
|
|
22
|
+
* hand-off, firing mid-materialization. Suspending first covers the wait, the
|
|
23
|
+
* lock hand-off, `load()`, and `register()` as one interval.
|
|
24
|
+
*
|
|
25
|
+
* Why the writer lock must be INSIDE: `withModelLoad` takes the exclusive
|
|
26
|
+
* writer slot, which excludes inference readers for exactly as long as
|
|
27
|
+
* weights are being materialized — no longer. Hoisting it outside the
|
|
28
|
+
* suspension would gain nothing and would hold readers off during the drain
|
|
29
|
+
* bookkeeping too.
|
|
30
|
+
*
|
|
31
|
+
* See `ServerInstance.withSuspendedDrains` and the `__internal__.clearCache`
|
|
32
|
+
* rustdoc in `packages/core/index.d.cts` for the underlying contracts.
|
|
33
|
+
*/
|
|
34
|
+
import type { ChatConfig } from '@mlx-node/core';
|
|
35
|
+
import type { IdleSweeper } from './idle-sweeper.js';
|
|
36
|
+
import type { ModelWorkCoordinator } from './model-work-coordinator.js';
|
|
37
|
+
import type { ModelRegistry, ServableModel } from './registry.js';
|
|
38
|
+
export interface GuardedLoadDeps {
|
|
39
|
+
/** Only `withSuspendedDrains` is used; typed narrowly so tests can double it. */
|
|
40
|
+
idleSweeper: Pick<IdleSweeper, 'withSuspendedDrains'>;
|
|
41
|
+
modelWorkCoordinator: ModelWorkCoordinator;
|
|
42
|
+
registry: ModelRegistry;
|
|
43
|
+
}
|
|
44
|
+
export interface LoadModelOptions {
|
|
45
|
+
/** Primary registration name. Also used as the `/health` load label. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** Materializes the model. Invoked exactly once, inside both brackets. */
|
|
48
|
+
load: () => Promise<ServableModel>;
|
|
49
|
+
/**
|
|
50
|
+
* Extra names bound to the SAME instance. Aliases share one
|
|
51
|
+
* `SessionRegistry`, preserving the single-warm invariant across names.
|
|
52
|
+
* An alias equal to `name` is ignored rather than re-registered.
|
|
53
|
+
*/
|
|
54
|
+
aliases?: string[];
|
|
55
|
+
/** Per-model sampling defaults; forwarded to every alias too. */
|
|
56
|
+
samplingDefaults?: ChatConfig;
|
|
57
|
+
/** Per-model output-token clamp; forwarded to every alias too. */
|
|
58
|
+
maxOutputTokens?: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load a model and register it under `name` (plus any aliases) with drains
|
|
62
|
+
* suspended and inference excluded for the whole materialization window.
|
|
63
|
+
*
|
|
64
|
+
* Rejects with the underlying error if `load()` (or `register`) throws;
|
|
65
|
+
* both brackets unwind via their own `finally`, so neither the suspend
|
|
66
|
+
* counter nor the writer lock can leak.
|
|
67
|
+
*/
|
|
68
|
+
export declare function runGuardedModelLoad(deps: GuardedLoadDeps, opts: LoadModelOptions): Promise<void>;
|
|
69
|
+
//# sourceMappingURL=load-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-model.d.ts","sourceRoot":"","sources":["../src/load-model.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAmB,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnF,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,qBAAqB,CAAC,CAAC;IACtD,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,IAAI,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,UAAU,CAAC;IAC9B,kEAAkE;IAClE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBtG"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe out-of-band model load.
|
|
3
|
+
*
|
|
4
|
+
* A supervisor (or a `/model`-picker UI) needs to swap the resident model on a
|
|
5
|
+
* server that is already serving. Doing that naively races the Metal
|
|
6
|
+
* allocator — a corruption/abort bug, not a latency bug — so the bracketing
|
|
7
|
+
* order below is load-bearing:
|
|
8
|
+
*
|
|
9
|
+
* ```text
|
|
10
|
+
* idleSweeper.withSuspendedDrains( ← OUTSIDE
|
|
11
|
+
* modelWorkCoordinator.withModelLoad( ← INSIDE
|
|
12
|
+
* load() ; registry.register(...)
|
|
13
|
+
* , label))
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Why the suspension must be OUTSIDE the writer lock: `endRequest()` arms a
|
|
17
|
+
* drain timer at `t + idleClearCacheMs` whose callback runs
|
|
18
|
+
* `__internal__.clearCache()`, walking the process-wide MLX free pool. A load
|
|
19
|
+
* that arrives while another load holds the writer slot parks inside
|
|
20
|
+
* `acquireWrite()` — and if the suspension were taken only after the lock was
|
|
21
|
+
* won, that armed timer would still be live during the wait AND during the
|
|
22
|
+
* hand-off, firing mid-materialization. Suspending first covers the wait, the
|
|
23
|
+
* lock hand-off, `load()`, and `register()` as one interval.
|
|
24
|
+
*
|
|
25
|
+
* Why the writer lock must be INSIDE: `withModelLoad` takes the exclusive
|
|
26
|
+
* writer slot, which excludes inference readers for exactly as long as
|
|
27
|
+
* weights are being materialized — no longer. Hoisting it outside the
|
|
28
|
+
* suspension would gain nothing and would hold readers off during the drain
|
|
29
|
+
* bookkeeping too.
|
|
30
|
+
*
|
|
31
|
+
* See `ServerInstance.withSuspendedDrains` and the `__internal__.clearCache`
|
|
32
|
+
* rustdoc in `packages/core/index.d.cts` for the underlying contracts.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Load a model and register it under `name` (plus any aliases) with drains
|
|
36
|
+
* suspended and inference excluded for the whole materialization window.
|
|
37
|
+
*
|
|
38
|
+
* Rejects with the underlying error if `load()` (or `register`) throws;
|
|
39
|
+
* both brackets unwind via their own `finally`, so neither the suspend
|
|
40
|
+
* counter nor the writer lock can leak.
|
|
41
|
+
*/
|
|
42
|
+
export async function runGuardedModelLoad(deps, opts) {
|
|
43
|
+
// Build the register options once, omitting keys the caller never set.
|
|
44
|
+
// `ModelRegistry.register` uses `'samplingDefaults' in opts` to decide
|
|
45
|
+
// whether to OVERWRITE an existing binding's defaults, so passing an
|
|
46
|
+
// explicit `undefined` would silently clear them on a re-register.
|
|
47
|
+
const registerOpts = {};
|
|
48
|
+
if (opts.samplingDefaults !== undefined)
|
|
49
|
+
registerOpts.samplingDefaults = opts.samplingDefaults;
|
|
50
|
+
if (opts.maxOutputTokens !== undefined)
|
|
51
|
+
registerOpts.maxOutputTokens = opts.maxOutputTokens;
|
|
52
|
+
await deps.idleSweeper.withSuspendedDrains(async () => {
|
|
53
|
+
await deps.modelWorkCoordinator.withModelLoad(async () => {
|
|
54
|
+
const instance = await opts.load();
|
|
55
|
+
deps.registry.register(opts.name, instance, registerOpts);
|
|
56
|
+
for (const alias of opts.aliases ?? []) {
|
|
57
|
+
if (alias === opts.name)
|
|
58
|
+
continue;
|
|
59
|
+
deps.registry.register(alias, instance, registerOpts);
|
|
60
|
+
}
|
|
61
|
+
}, opts.name);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelLoadRecord } from './health.js';
|
|
1
2
|
/**
|
|
2
3
|
* Result of a `withModelLoad` call that surfaces who actually drove the load
|
|
3
4
|
* vs. who merely parked behind one that was already in flight. Callers use
|
|
@@ -41,11 +42,35 @@ export interface ModelLoadOutcome<T> {
|
|
|
41
42
|
*/
|
|
42
43
|
export declare class ModelWorkCoordinator {
|
|
43
44
|
private activeReaders;
|
|
44
|
-
private
|
|
45
|
-
private
|
|
45
|
+
private writerHeld;
|
|
46
|
+
private queuedWriters;
|
|
46
47
|
private readonly readerWaiters;
|
|
47
48
|
private readonly writerWaiters;
|
|
48
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Most recent settled load bracket. Retained here because the coordinator
|
|
51
|
+
* is the ONE place that brackets every load: a `resolveModel` failure in
|
|
52
|
+
* `/v1/messages` becomes a 500 and is otherwise dropped on the floor, so a
|
|
53
|
+
* supervisor polling `/health` afterwards had no way to learn what went
|
|
54
|
+
* wrong. See {@link ModelLoadRecord} for the "successful no-op overwrites
|
|
55
|
+
* an earlier failure" caveat.
|
|
56
|
+
*/
|
|
57
|
+
private lastLoadRecord;
|
|
58
|
+
/** Read-only: `true` while a load holds the exclusive writer slot. */
|
|
59
|
+
get writerActive(): boolean;
|
|
60
|
+
/** Read-only: loads parked in `acquireWrite()` waiting for the slot. */
|
|
61
|
+
get waitingWriters(): number;
|
|
62
|
+
/** Read-only: outcome of the most recent settled load bracket, or `null`. */
|
|
63
|
+
get lastLoad(): ModelLoadRecord | null;
|
|
64
|
+
/**
|
|
65
|
+
* Record a settled bracket. Called from the `finally` of both load
|
|
66
|
+
* wrappers so a throw is captured just as reliably as a success.
|
|
67
|
+
*/
|
|
68
|
+
private recordLoad;
|
|
69
|
+
/**
|
|
70
|
+
* @param label Optional identifier (normally the model name) stamped into
|
|
71
|
+
* {@link lastLoad} so `/health` can name what was being loaded.
|
|
72
|
+
*/
|
|
73
|
+
withModelLoad<T>(fn: () => Promise<T> | T, label?: string): Promise<T>;
|
|
49
74
|
/**
|
|
50
75
|
* Like {@link withModelLoad} but reports whether THIS caller owned the
|
|
51
76
|
* load (acquired the writer lock with no contention) or merely waited
|
|
@@ -59,7 +84,7 @@ export declare class ModelWorkCoordinator {
|
|
|
59
84
|
* 60-second cold-load does not look like 60 seconds of own work for
|
|
60
85
|
* every concurrent request.
|
|
61
86
|
*/
|
|
62
|
-
withModelLoadInstrumented<T>(fn: () => Promise<T> | T): Promise<ModelLoadOutcome<T>>;
|
|
87
|
+
withModelLoadInstrumented<T>(fn: () => Promise<T> | T, label?: string): Promise<ModelLoadOutcome<T>>;
|
|
63
88
|
withInference<T>(fn: () => Promise<T> | T): Promise<T>;
|
|
64
89
|
private acquireRead;
|
|
65
90
|
private acquireWrite;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-work-coordinator.d.ts","sourceRoot":"","sources":["../src/model-work-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC;IACV,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"model-work-coordinator.d.ts","sourceRoot":"","sources":["../src/model-work-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAqBnD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,CAAC,CAAC;IACV,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyB;IACvD;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc,CAAgC;IAEtD,sEAAsE;IACtE,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,wEAAwE;IACxE,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,6EAA6E;IAC7E,IAAI,QAAQ,IAAI,eAAe,GAAG,IAAI,CAErC;IAED;;;OAGG;IACH,OAAO,CAAC,UAAU;IAUlB;;;OAGG;IACG,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAmB3E;IAED;;;;;;;;;;;;OAYG;IACG,yBAAyB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CA2BzG;IAEK,aAAa,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO3D;IAED,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,YAAY;IAgBpB,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,KAAK;CAWd"}
|