@gjsify/model-store 0.64.25 → 0.64.26-next.312
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/README.md +19 -0
- package/lib/esm/download.js +1 -0
- package/lib/esm/index.js +1 -1
- package/lib/esm/store.js +1 -1
- package/lib/types/download.d.ts +63 -0
- package/lib/types/download.spec.d.ts +2 -0
- package/lib/types/index.d.ts +2 -1
- package/lib/types/store.d.ts +2 -21
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -51,6 +51,25 @@ Digests: a downloaded file is hashed while it streams and only renamed into plac
|
|
|
51
51
|
|
|
52
52
|
The store never answers `unavailable` itself: that is the capability's call (no engine, or `checkFit` from `@gjsify/machine-fit` says `none`).
|
|
53
53
|
|
|
54
|
+
## Downloading a file you did not plan for: `downloadVerified`
|
|
55
|
+
|
|
56
|
+
Not every model comes from a fixed manifest. For a one-off download that still needs streaming verification and an atomic rename, use the standalone helper:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { downloadVerified } from '@gjsify/model-store';
|
|
60
|
+
|
|
61
|
+
const { bytes, sha256 } = await downloadVerified({
|
|
62
|
+
url: 'https://example.org/one-off.bin',
|
|
63
|
+
dest: '/tmp/one-off.bin',
|
|
64
|
+
bytes: 2313101,
|
|
65
|
+
sha256: '6b99cbfd39246b6706f98ec13c7c50c6b299181f2474fa05cbc8046acc274396',
|
|
66
|
+
headers: { Authorization: 'Bearer token' },
|
|
67
|
+
monitor: (m) => m.addEventListener('downloadprogress', (e) => console.log(e.loaded)),
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`bytes` and `sha256` are optional checks, but the returned values always report what was actually received. The file is written under a temporary name and renamed into place only after the checks pass; failures remove the partial file.
|
|
72
|
+
|
|
54
73
|
## License
|
|
55
74
|
|
|
56
75
|
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{ProgressFanout as e}from"./progress.js";import{dirname as t}from"node:path";import{createHash as n,randomUUID as r}from"node:crypto";import{closeSync as i,mkdirSync as a,openSync as o,renameSync as s,unlinkSync as c,writeSync as l}from"node:fs";function removeQuietly(e){try{c(e)}catch{}}function prefix(e){return e===void 0?`Downloading`:`Downloading ${e}`}async function downloadVerifiedInternal(c){let{url:u,dest:d,bytes:f,sha256:p,headers:m,signal:h,fetch:g}=c,_=await(g??((e,t)=>fetch(e,t)))(u,{signal:h,redirect:`follow`,headers:m});if(!_.ok)throw Error(`${prefix(c.id)} from ${u} failed: HTTP ${_.status}`);let v=_.headers.get(`content-length`);if(v!==null&&f!==void 0&&Number(v)!==f){let e=c.id===void 0?`expected`:`the manifest says`;throw Error(`${prefix(c.id)} refused: ${u} declares ${v} bytes, ${e} ${f}`)}a(t(d),{recursive:!0});let y=`${d}.part-${r()}`,b=o(y,`w`),x=!1,S=c.progress??new e(c.id??u);c.progress===void 0&&c.monitor!==void 0&&S.add(c.monitor);try{let e=n(`sha256`),t=0,r=f,write=n=>{if(t+=n.length,f!==void 0&&t>f){let e=c.id===void 0?`the expected`:`the manifest's`;throw Error(`${prefix(c.id)} failed: more than ${e} ${f} bytes`)}e.update(n),l(b,n),S.report(t,r??t)};S.report(0,r??0);let a=_.body?.getReader();if(a!==void 0)try{for(;;){h?.throwIfAborted();let{done:e,value:t}=await a.read();if(e)break;t!==void 0&&t.length>0&&write(t)}}catch(e){throw await a.cancel().catch(()=>void 0),e}else write(new Uint8Array(await _.arrayBuffer()));if(f!==void 0&&t!==f)throw Error(`${prefix(c.id)} failed: got ${t} bytes, expected ${f}`);let o=e.digest(`hex`);if(p!==void 0&&o!==p)throw Error(`${prefix(c.id)} failed: sha256 ${o}, expected ${p}`);return i(b),x=!0,s(y,d),S.report(t,r??t),{bytes:t,sha256:o}}catch(e){throw x||i(b),removeQuietly(y),e}}async function downloadVerified(e){return downloadVerifiedInternal(e)}export{downloadVerified,downloadVerifiedInternal};
|
package/lib/esm/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{modelsRoot as e,userCacheDir as t}from"./cache-dir.js";import{
|
|
1
|
+
import{modelsRoot as e,userCacheDir as t}from"./cache-dir.js";import{DownloadProgressEvent as n}from"./progress.js";import{downloadVerified as r}from"./download.js";import{validateManifest as i}from"./manifest.js";import{ModelStore as a,sha256File as o}from"./store.js";export{n as DownloadProgressEvent,a as ModelStore,r as downloadVerified,e as modelsRoot,o as sha256File,t as userCacheDir,i as validateManifest};
|
package/lib/esm/store.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{modelsRoot as e}from"./cache-dir.js";import{
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{modelsRoot as e}from"./cache-dir.js";import{ProgressFanout as t}from"./progress.js";import{downloadVerifiedInternal as n}from"./download.js";import{validateManifest as r}from"./manifest.js";import{join as i}from"node:path";import{createHash as a,randomUUID as o}from"node:crypto";import{copyFileSync as s,mkdirSync as c,renameSync as l,statSync as u,unlinkSync as d}from"node:fs";import{open as f}from"node:fs/promises";const p=/^[a-z0-9][a-z0-9-]*$/;function sizeOf(e){try{let t=u(e);return t.isFile()?t.size:null}catch{return null}}function removeQuietly(e){try{d(e)}catch{}}async function sha256File(e,t){let n=a(`sha256`),r=await f(e,`r`);try{let e=new Uint8Array(1024*1024);for(;;){t?.throwIfAborted();let{bytesRead:i}=await r.read(e,0,e.length,null);if(i===0)break;n.update(e.subarray(0,i))}}finally{await r.close()}return n.digest(`hex`)}var ModelStore=class{capability;models;defaultModel;#e;#t;#n;#r;#i=new Map;#a=new Map;constructor(e){if(!p.test(e.capability))throw Error(`ModelStore: capability '${e.capability}' is not a lowercase directory name`);if(e.models.length===0)throw Error(`ModelStore '${e.capability}': no models`);r(e.models),this.capability=e.capability,this.models=e.models,this.defaultModel=e.defaultModel??e.models[0].id,this.#e=e.root,this.#t=e.appDataDirs??[];let t=e.legacyDirs??[];this.#n=typeof t==`function`?t:()=>t,this.#r=e.fetch,this.require(this.defaultModel)}get cacheDir(){return i(this.#e??e(),this.capability)}find(e=this.defaultModel){return this.models.find(t=>t.id===e)}require(e=this.defaultModel){let t=this.find(e);if(t===void 0){let t=this.models.map(e=>e.id).join(`, `);throw RangeError(`Unknown ${this.capability} model '${e}' — known ids: ${t}`)}return t}cachePath(e=this.defaultModel){return i(this.cacheDir,this.require(e).file)}locate(e=this.defaultModel,t={}){let n=this.require(e);if(t.path!==void 0){let e=sizeOf(t.path);if(e!==n.bytes){let r=e===null?`no file`:`${e} bytes`;throw Error(`${this.capability} model '${n.id}': ${t.path} has ${r}, expected ${n.bytes}`)}return{path:t.path,source:`configured`}}for(let e of this.#t){let t=i(e,n.file);if(sizeOf(t)===n.bytes)return{path:t,source:`app-data`}}let r=i(this.cacheDir,n.file);if(sizeOf(r)===n.bytes||this.#o(n,r))return{path:r,source:`cache`}}availability(e=this.defaultModel,t={}){let n=this.require(e);return this.locate(n.id,t)===void 0?this.#i.has(n.id)?`downloading`:`downloadable`:`available`}installed(){return this.models.filter(e=>this.locate(e.id)!==void 0).map(e=>e.id)}async resolve(e=this.defaultModel,t={}){let n=this.require(e),r=this.locate(n.id,t);if(r===void 0)throw Error(`${this.capability} model '${n.id}' is not installed — await install('${n.id}') first. ${n.bytes} bytes from ${n.url}.`);return r.source!==`cache`&&await this.#s(n,r.path,t.signal),r.path}install(e=this.defaultModel,n={}){let r;try{if(r=this.require(e),!n.force&&this.locate(r.id,n)!==void 0){let e=new t(r.id);return e.add(n.monitor),this.resolve(r.id,n).then(t=>(e.report(r.bytes,r.bytes),t))}}catch(e){return Promise.reject(e)}let i=this.#i.get(r.id);if(i!==void 0)return i.progress.add(n.monitor),i.promise;let a=new t(r.id);a.add(n.monitor);let o=this.#c(r,a,n).finally(()=>this.#i.delete(r.id));return this.#i.set(r.id,{promise:o,progress:a}),o}uninstall(e=this.defaultModel){let t=this.cachePath(e);return sizeOf(t)===null?!1:(d(t),!0)}#o(e,t){for(let n of this.#n()){let r=i(n,e.file);if(sizeOf(r)===e.bytes)try{c(this.cacheDir,{recursive:!0});try{l(r,t)}catch{let e=`${t}.part-${o()}`;try{s(r,e),l(e,t)}catch(t){throw removeQuietly(e),t}removeQuietly(r)}return!0}catch{return!1}}return!1}async#s(e,t,n){let r=u(t),i=`${r.size}:${r.mtimeMs}`;if(this.#a.get(t)===i)return;let a=await sha256File(t,n);if(a!==e.sha256)throw Error(`${this.capability} model '${e.id}': ${t} has sha256 ${a}, expected ${e.sha256}`);this.#a.set(t,i)}async#c(e,t,r){let a=i(this.cacheDir,e.file);return await n({url:e.url,dest:a,bytes:e.bytes,sha256:e.sha256,signal:r.signal,fetch:r.fetch??this.#r,progress:t,id:e.id}),a}};export{ModelStore,sha256File};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { CreateMonitorCallback, ProgressFanout } from './progress.js';
|
|
2
|
+
/** The subset of `fetch` the downloader uses, so a test can inject one. */
|
|
3
|
+
export type FetchLike = (url: string, init: {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
redirect?: 'follow';
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
ok: boolean;
|
|
9
|
+
status: number;
|
|
10
|
+
headers: {
|
|
11
|
+
get(name: string): string | null;
|
|
12
|
+
};
|
|
13
|
+
body?: {
|
|
14
|
+
getReader(): {
|
|
15
|
+
read(): Promise<{
|
|
16
|
+
done: boolean;
|
|
17
|
+
value?: Uint8Array;
|
|
18
|
+
}>;
|
|
19
|
+
cancel(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
} | null;
|
|
22
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
23
|
+
}>;
|
|
24
|
+
export interface DownloadVerifiedOptions {
|
|
25
|
+
/** URL to fetch. */
|
|
26
|
+
url: string;
|
|
27
|
+
/** Final path; parent directories are created. */
|
|
28
|
+
dest: string;
|
|
29
|
+
/** Expected size in bytes. Checked while streaming and after. */
|
|
30
|
+
bytes?: number;
|
|
31
|
+
/** Expected SHA-256, lowercase hex. Checked after the stream finishes. */
|
|
32
|
+
sha256?: string;
|
|
33
|
+
/** Extra request headers. */
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
/** Receives an EventTarget firing `downloadprogress`, at least at 0 and 1. */
|
|
36
|
+
monitor?: CreateMonitorCallback;
|
|
37
|
+
/** Abort the request and close the partial file. */
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
/** Override the global `fetch`. */
|
|
40
|
+
fetch?: FetchLike;
|
|
41
|
+
}
|
|
42
|
+
interface DownloadVerifiedInternalOptions extends DownloadVerifiedOptions {
|
|
43
|
+
/** Identifier used in error messages. Omit for the standalone public function. */
|
|
44
|
+
id?: string;
|
|
45
|
+
/** Existing progress fanout (e.g. ModelStore's shared one). */
|
|
46
|
+
progress?: ProgressFanout;
|
|
47
|
+
}
|
|
48
|
+
/** @internal Variant that lets ModelStore keep its existing error wording. */
|
|
49
|
+
export declare function downloadVerifiedInternal(options: DownloadVerifiedInternalOptions): Promise<{
|
|
50
|
+
bytes: number;
|
|
51
|
+
sha256: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Stream `url` to `dest`, verifying size and/or SHA-256 while downloading.
|
|
55
|
+
* The file is written under a temporary name and renamed into place only after
|
|
56
|
+
* verification. Returns the measured size and digest. Removes the partial file
|
|
57
|
+
* on any failure or abort.
|
|
58
|
+
*/
|
|
59
|
+
export declare function downloadVerified(options: DownloadVerifiedOptions): Promise<{
|
|
60
|
+
bytes: number;
|
|
61
|
+
sha256: string;
|
|
62
|
+
}>;
|
|
63
|
+
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { modelsRoot, userCacheDir } from './cache-dir.js';
|
|
2
|
+
export { downloadVerified, type DownloadVerifiedOptions, type FetchLike } from './download.js';
|
|
2
3
|
export { validateManifest, type ModelEntry } from './manifest.js';
|
|
3
4
|
export { DownloadProgressEvent, type CreateMonitorCallback } from './progress.js';
|
|
4
|
-
export { ModelStore, sha256File, type
|
|
5
|
+
export { ModelStore, sha256File, type InstallOptions, type LocatedModel, type LocateOptions, type ModelAvailability, type ModelSource, type ModelStoreOptions, type ResolveOptions, } from './store.js';
|
package/lib/types/store.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type FetchLike } from './download.js';
|
|
1
2
|
import { type ModelEntry } from './manifest.js';
|
|
2
3
|
import { type CreateMonitorCallback } from './progress.js';
|
|
4
|
+
export { type FetchLike };
|
|
3
5
|
/**
|
|
4
6
|
* The WebML CG availability states. The store itself never answers
|
|
5
7
|
* `unavailable` — that is the capability's verdict (no engine, or `checkFit`
|
|
@@ -12,27 +14,6 @@ export interface LocatedModel {
|
|
|
12
14
|
readonly path: string;
|
|
13
15
|
readonly source: ModelSource;
|
|
14
16
|
}
|
|
15
|
-
/** The subset of `fetch` the store uses, so a test can inject one. */
|
|
16
|
-
export type FetchLike = (url: string, init: {
|
|
17
|
-
signal?: AbortSignal;
|
|
18
|
-
redirect?: 'follow';
|
|
19
|
-
}) => Promise<{
|
|
20
|
-
ok: boolean;
|
|
21
|
-
status: number;
|
|
22
|
-
headers: {
|
|
23
|
-
get(name: string): string | null;
|
|
24
|
-
};
|
|
25
|
-
body?: {
|
|
26
|
-
getReader(): {
|
|
27
|
-
read(): Promise<{
|
|
28
|
-
done: boolean;
|
|
29
|
-
value?: Uint8Array;
|
|
30
|
-
}>;
|
|
31
|
-
cancel(): Promise<void>;
|
|
32
|
-
};
|
|
33
|
-
} | null;
|
|
34
|
-
arrayBuffer(): Promise<ArrayBuffer>;
|
|
35
|
-
}>;
|
|
36
17
|
export interface ModelStoreOptions<E extends ModelEntry> {
|
|
37
18
|
/** Directory name under the models root: `speech`, `embeddings`, `image`. Lowercase, digits, dashes. */
|
|
38
19
|
readonly capability: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/model-store",
|
|
3
|
-
"version": "0.64.
|
|
3
|
+
"version": "0.64.26-next.312",
|
|
4
4
|
"description": "Where an AI model lives and how it gets there: one store per capability with streaming download, SHA-256 verification, WebML-shaped download progress, and three sources in order (a configured path, an app's read-only data dir, the per-user cache). Pure TypeScript on node:* APIs.",
|
|
5
5
|
"module": "lib/esm/index.js",
|
|
6
6
|
"types": "lib/types/index.d.ts",
|
|
@@ -39,15 +39,15 @@
|
|
|
39
39
|
"gguf"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@gjsify/crypto": "
|
|
43
|
-
"@gjsify/fs": "
|
|
44
|
-
"@gjsify/os": "
|
|
45
|
-
"@gjsify/path": "
|
|
46
|
-
"@gjsify/process": "
|
|
42
|
+
"@gjsify/crypto": "0.64.26-next.312",
|
|
43
|
+
"@gjsify/fs": "0.64.26-next.312",
|
|
44
|
+
"@gjsify/os": "0.64.26-next.312",
|
|
45
|
+
"@gjsify/path": "0.64.26-next.312",
|
|
46
|
+
"@gjsify/process": "0.64.26-next.312"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@gjsify/cli": "
|
|
50
|
-
"@gjsify/unit": "
|
|
49
|
+
"@gjsify/cli": "0.64.26-next.312",
|
|
50
|
+
"@gjsify/unit": "0.64.26-next.312",
|
|
51
51
|
"@types/node": "^25.9.2",
|
|
52
52
|
"typescript": "^6.0.3"
|
|
53
53
|
},
|
|
@@ -78,5 +78,6 @@
|
|
|
78
78
|
"win32": "The cache root is %LOCALAPPDATA%\\gjsify\\models, where GLib's get_user_cache_dir() would answer the Internet cache folder."
|
|
79
79
|
},
|
|
80
80
|
"headless": true
|
|
81
|
-
}
|
|
81
|
+
},
|
|
82
|
+
"gitHead": "75fa6155a8f1fa0f7e5d70c43418ae41ecac8c0e"
|
|
82
83
|
}
|