@qvac/registry-client 0.3.0 → 0.4.1

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/NOTICE CHANGED
@@ -51,42 +51,6 @@ Third-Party Model Licenses
51
51
  https://huggingface.co/personalizedrefrigerator/whisper-base-fr
52
52
  fr-tiny-ggml-model-f16
53
53
  https://huggingface.co/JaepaX/whisper-tiny-fr
54
- ggml-opus-ar-en
55
- https://huggingface.co/Helsinki-NLP/opus-mt-ar-en
56
- ggml-opus-de-en
57
- https://huggingface.co/Helsinki-NLP/opus-mt-de-en
58
- ggml-opus-de-fr
59
- https://huggingface.co/Helsinki-NLP/opus-mt-de-fr
60
- ggml-opus-en-ar
61
- https://huggingface.co/Helsinki-NLP/opus-mt-en-ar
62
- ggml-opus-en-es
63
- https://huggingface.co/Helsinki-NLP/opus-mt-en-es
64
- ggml-opus-en-fr
65
- https://huggingface.co/Helsinki-NLP/opus-mt-en-fr
66
- ggml-opus-en-it
67
- https://huggingface.co/Helsinki-NLP/opus-mt-en-it
68
- ggml-opus-en-ru
69
- https://huggingface.co/Helsinki-NLP/opus-mt-en-ru
70
- ggml-opus-en-zh
71
- https://huggingface.co/Helsinki-NLP/opus-mt-en-zh
72
- ggml-opus-es-en
73
- https://huggingface.co/Helsinki-NLP/opus-mt-es-en
74
- ggml-opus-es-fr
75
- https://huggingface.co/Helsinki-NLP/opus-mt-es-fr
76
- ggml-opus-fr-de
77
- https://huggingface.co/Helsinki-NLP/opus-mt-fr-de
78
- ggml-opus-fr-en
79
- https://huggingface.co/Helsinki-NLP/opus-mt-fr-en
80
- ggml-opus-fr-es
81
- https://huggingface.co/Helsinki-NLP/opus-mt-fr-es
82
- ggml-opus-it-en
83
- https://huggingface.co/Helsinki-NLP/opus-mt-it-en
84
- ggml-opus-ja-en
85
- https://huggingface.co/Helsinki-NLP/opus-mt-ja-en
86
- ggml-opus-mt-en-roa-f16
87
- https://huggingface.co/Helsinki-NLP/opus-mt-en-roa
88
- ggml-opus-mt-roa-en-f16
89
- https://huggingface.co/Helsinki-NLP/opus-mt-roa-en
90
54
  gpt-oss-20b-GGUF
91
55
  https://huggingface.co/unsloth/gpt-oss-20b-GGUF
92
56
  it-base-ggml-model-f16
@@ -128,17 +92,6 @@ Third-Party Model Licenses
128
92
  SmolVLM2-500M-Video-Instruct-GGUF
129
93
  https://huggingface.co/ggml-org/SmolVLM2-500M-Video-Instruct-GGUF
130
94
 
131
- --- cc-by-4.0 (Creative Commons Attribution 4.0 International) ---
132
-
133
- ggml-opus-en-de
134
- https://huggingface.co/Helsinki-NLP/opus-mt-en-de
135
- ggml-opus-en-pt
136
- https://huggingface.co/Helsinki-NLP/opus-mt-tc-big-en-pt
137
- ggml-opus-ru-en
138
- https://huggingface.co/Helsinki-NLP/opus-mt-ru-en
139
- ggml-opus-zh-en
140
- https://huggingface.co/Helsinki-NLP/opus-mt-zh-en
141
-
142
95
  --- gemma (Gemma Terms of Use) ---
143
96
 
144
97
  embeddinggemma-300m-GGUF
package/README.md CHANGED
@@ -139,11 +139,9 @@ The package includes a CLI for querying and downloading models from the registry
139
139
 
140
140
  ### Install
141
141
 
142
- The package is hosted on npm. Configure npm to use the registry for the `@qvac` scope, then install globally:
142
+ The package is hosted on npm. Install globally:
143
143
 
144
144
  ```bash
145
- echo "@qvac:registry=https://registry.npmjs.org" >> ~/.npmrc
146
- echo "//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN" >> ~/.npmrc
147
145
  npm install -g @qvac/registry-client
148
146
  ```
149
147
 
package/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import ReadyResource = require('ready-resource')
2
+
3
+ export interface LifecycleLogOptions {
4
+ log?: (msg: string) => Promise<void> | void
5
+ }
6
+
1
7
  export interface QVACBlobBinding {
2
8
  coreKey: Buffer | string
3
9
  blockOffset: number
@@ -41,6 +47,8 @@ export interface QVACDownloadOptions {
41
47
  timeout?: number
42
48
  peerTimeout?: number
43
49
  outputFile?: string
50
+ onProgress?: (progress: { downloaded: number, total: number, cachedBlocks: number, totalBlocks: number }) => void
51
+ signal?: AbortSignal
44
52
  }
45
53
 
46
54
  export interface QVACBlobDownloadOptions {
@@ -58,6 +66,7 @@ export interface QVACRegistryClientOptions {
58
66
  storage?: string
59
67
  registryCoreKey?: string
60
68
  logger?: any
69
+ corestoreOpts?: { wait?: boolean }
61
70
  }
62
71
 
63
72
  export interface QVACModelQuery {
@@ -78,11 +87,29 @@ export interface FindByParams {
78
87
  includeDeprecated?: boolean
79
88
  }
80
89
 
81
- export class QVACRegistryClient {
90
+ export interface LifecycleSwarmHandle {
91
+ readonly suspended: boolean
92
+ suspend (opts?: LifecycleLogOptions): Promise<void>
93
+ resume (opts?: LifecycleLogOptions): Promise<void>
94
+ }
95
+
96
+ export interface LifecycleStoreHandle {
97
+ suspend (opts?: LifecycleLogOptions): Promise<void>
98
+ resume (): Promise<void>
99
+ }
100
+
101
+ export class QVACRegistryClient extends ReadyResource {
82
102
  constructor (opts?: QVACRegistryClientOptions)
83
103
 
104
+ /** Valid only while the client remains open. Cached handles become stale after close(). */
105
+ readonly corestore: LifecycleStoreHandle | null
106
+ /** Valid only while the client remains open. Cached handles become stale after close(). */
107
+ readonly hyperswarm: LifecycleSwarmHandle | null
108
+
84
109
  ready (): Promise<void>
85
110
  close (): Promise<void>
111
+ suspend (opts?: LifecycleLogOptions): Promise<void>
112
+ resume (opts?: LifecycleLogOptions): Promise<void>
86
113
 
87
114
  getModel (path: string, source: string): Promise<QVACModelEntry | null>
88
115
  downloadModel (path: string, source: string, options?: QVACDownloadOptions): Promise<QVACDownloadResult>
package/lib/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { QvacErrorRegistryClient, ERR_CODES } = require('../utils/error')
4
+ const { withRetry } = require('../utils/retry')
4
5
  const RegistryConfig = require('./config')
5
6
  const { RegistryDatabase } = require('@qvac/registry-schema')
6
7
  const ReadyResource = require('ready-resource')
@@ -12,6 +13,9 @@ const IdEnc = require('hypercore-id-encoding')
12
13
  const path = require('#path')
13
14
  const fs = require('#fs')
14
15
 
16
+ const DEFAULT_DOWNLOAD_MAX_RETRIES = 3
17
+ const RETRIABLE_DOWNLOAD_CODES = ['REQUEST_TIMEOUT']
18
+
15
19
  class QVACRegistryClient extends ReadyResource {
16
20
  constructor (opts = {}) {
17
21
  super()
@@ -20,13 +24,13 @@ class QVACRegistryClient extends ReadyResource {
20
24
  this.registryConfig = new RegistryConfig({ logger: this.logger })
21
25
 
22
26
  this.db = null
23
- this.corestore = null
24
27
  this.hyperswarm = null
25
28
  this._connectionHandler = null
26
29
  this._metadataReady = null
27
30
 
28
31
  this.storage = this.registryConfig.getRegistryStorage(opts.storage)
29
32
  this.registryCoreKey = this.registryConfig.getRegistryCoreKey(opts.registryCoreKey)
33
+ this.corestore = new Corestore(this.storage, opts.corestoreOpts || {})
30
34
 
31
35
  this.logger.debug('Initializing QVAC Registry Client', {
32
36
  mode: 'read',
@@ -39,8 +43,7 @@ class QVACRegistryClient extends ReadyResource {
39
43
  async _open () {
40
44
  this.logger.debug('_open called')
41
45
 
42
- this.logger.debug('Creating corestore for open')
43
- this.corestore = new Corestore(this.storage)
46
+ this.logger.debug('Opening corestore')
44
47
  await this.corestore.ready()
45
48
 
46
49
  this.logger.debug('Creating Hyperswarm for open')
@@ -257,7 +260,15 @@ class QVACRegistryClient extends ReadyResource {
257
260
 
258
261
  let artifact
259
262
  if (options.outputFile) {
260
- await this._streamBlobToFile(blobs, core, model.blobBinding, options.outputFile, options)
263
+ await withRetry(
264
+ () => this._streamBlobToFile(blobs, core, model.blobBinding, options.outputFile, options),
265
+ {
266
+ maxRetries: options.maxRetries != null ? options.maxRetries : DEFAULT_DOWNLOAD_MAX_RETRIES,
267
+ retryCodes: RETRIABLE_DOWNLOAD_CODES,
268
+ onRetry: () => fs.promises.unlink(options.outputFile).catch(() => {}),
269
+ logger: this.logger
270
+ }
271
+ )
261
272
  artifact = { path: options.outputFile, totalSize }
262
273
 
263
274
  rangeDownload.destroy()
@@ -381,7 +392,15 @@ class QVACRegistryClient extends ReadyResource {
381
392
 
382
393
  let artifact
383
394
  if (options.outputFile) {
384
- await this._streamBlobToFile(blobs, core, pointer, options.outputFile, options)
395
+ await withRetry(
396
+ () => this._streamBlobToFile(blobs, core, pointer, options.outputFile, options),
397
+ {
398
+ maxRetries: options.maxRetries != null ? options.maxRetries : DEFAULT_DOWNLOAD_MAX_RETRIES,
399
+ retryCodes: RETRIABLE_DOWNLOAD_CODES,
400
+ onRetry: () => fs.promises.unlink(options.outputFile).catch(() => {}),
401
+ logger: this.logger
402
+ }
403
+ )
385
404
  artifact = { path: options.outputFile, totalSize }
386
405
 
387
406
  rangeDownload.destroy()
@@ -526,6 +545,42 @@ class QVACRegistryClient extends ReadyResource {
526
545
  }
527
546
  }
528
547
 
548
+ async suspend (opts = {}) {
549
+ this.logger.debug('suspend called')
550
+
551
+ if (!this.opened || this.closing) {
552
+ this.logger.debug('Skipping suspend while client is not open or is closing')
553
+ return
554
+ }
555
+
556
+ if (this.hyperswarm && !this.hyperswarm.suspended) {
557
+ await this.hyperswarm.suspend(opts)
558
+ }
559
+ if (this.corestore) {
560
+ await this.corestore.suspend(opts)
561
+ }
562
+
563
+ this.logger.debug('QVACRegistryClient suspended')
564
+ }
565
+
566
+ async resume (opts = {}) {
567
+ this.logger.debug('resume called')
568
+
569
+ if (!this.opened || this.closing) {
570
+ this.logger.debug('Skipping resume while client is not open or is closing')
571
+ return
572
+ }
573
+
574
+ if (this.corestore) {
575
+ await this.corestore.resume()
576
+ }
577
+ if (this.hyperswarm && this.hyperswarm.suspended) {
578
+ await this.hyperswarm.resume(opts)
579
+ }
580
+
581
+ this.logger.debug('QVACRegistryClient resumed')
582
+ }
583
+
529
584
  async _close () {
530
585
  this.logger.debug('_close called')
531
586
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/registry-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "QVAC Registry client library for read-only queries via Hyperswarm",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@qvac/error": "^0.1.0",
56
- "@qvac/registry-schema": "^0.1.1",
56
+ "@qvac/registry-schema": "^0.1.2",
57
57
  "b4a": "^1.6.7",
58
58
  "bare-fs": "^4.5.2",
59
59
  "bare-os": "^3.6.2",
@@ -62,8 +62,8 @@
62
62
  "corestore": "^7.4.5",
63
63
  "hyperblobs": "^2.8.0",
64
64
  "hypercore-id-encoding": "^1.3.0",
65
- "hyperdb": "^4.16.1",
66
65
  "hypercore-stats": "^2.4.0",
66
+ "hyperdb": "^4.16.1",
67
67
  "hyperswarm": "^4.14.0",
68
68
  "hyperswarm-stats": "^1.3.0",
69
69
  "paparam": "^1.10.0",
package/utils/retry.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Executes an async function with automatic retry on specific error codes.
5
+ *
6
+ * @param {() => Promise<unknown>} fn - The async operation to attempt
7
+ * @param {object} opts
8
+ * @param {number} [opts.maxRetries=3] - Maximum number of attempts (including the first)
9
+ * @param {string[]} [opts.retryCodes=[]] - Error codes that trigger a retry
10
+ * @param {() => Promise<void>} [opts.onRetry] - Called before each retry attempt (e.g. cleanup)
11
+ * @param {{ warn: Function }} [opts.logger] - Logger instance for retry warnings
12
+ * @returns {Promise<unknown>}
13
+ */
14
+ async function withRetry (fn, opts = {}) {
15
+ const { maxRetries = 3, retryCodes = [], onRetry, logger } = opts
16
+
17
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
18
+ try {
19
+ return await fn()
20
+ } catch (err) {
21
+ const isRetriable = retryCodes.length > 0 && retryCodes.includes(err && err.code)
22
+ if (!isRetriable || attempt >= maxRetries) throw err
23
+ logger && logger.warn(`Retrying after ${err.code} (attempt ${attempt}/${maxRetries}): ${err.message}`)
24
+ if (onRetry) await onRetry()
25
+ }
26
+ }
27
+ }
28
+
29
+ module.exports = { withRetry }