@quillmark/quiver 0.15.0 → 0.17.0

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/node.js CHANGED
@@ -1,75 +1,70 @@
1
1
  /**
2
- * Node-only entrypoint.
2
+ * Node-only entrypoint: the factories that read a filesystem, as free
3
+ * functions. Each returns a `Quiver`; the class itself is the browser-safe one
4
+ * from the main entry, unmodified. Importing this module installs nothing, so
5
+ * the import path is the whole contract and a bundler drops what is not called.
3
6
  *
4
- * Importing this module is the consumer's explicit declaration of intent:
5
- * "I am running in Node and want the Node-only Quiver factories." It exposes
6
- * the same `Quiver` class as the main entry, augmented with `fromDir`,
7
- * `fromPackage`, `fromBuiltDir`, `build`, and `buildPackage` static methods.
7
+ * Bundler note: this entry pulls in `./source-loader.js`, `./build.js`, and
8
+ * `./transports/fs-built-transport.js`, all of which statically import `node:*`
9
+ * builtins. Browser bundles must never reach it. The main entry (`./index.js`)
10
+ * makes no static or dynamic reference to it.
11
+ */
12
+ import { createQuiver } from './quiver.js';
13
+ import { scanSourceQuiver, SourceLoader } from './source-loader.js';
14
+ import { buildQuiver } from './build.js';
15
+ import { loadBuiltQuiver } from './built-loader.js';
16
+ import { FsBuiltTransport } from './transports/fs-built-transport.js';
17
+ import { fileURLToPath } from 'node:url';
18
+ /**
19
+ * Reads a Source Quiver from a local directory containing `Quiver.yaml` and
20
+ * `quills/<name>/<version>/Quill.yaml` entries.
21
+ *
22
+ * Also accepts an `import.meta.url`-style `file://` URL; the URL's parent
23
+ * directory is used as the source root.
8
24
  *
9
- * Side effect: at module evaluation time, the Node-only static methods are
10
- * installed on the shared `Quiver` constructor. Any other module that already
11
- * imports `Quiver` from the main entry will see the additional methods at
12
- * runtime — but TypeScript will only expose them on the binding imported from
13
- * here, so the import path remains the contract.
25
+ * A quiver installed from npm is a directory like any other:
26
+ * `dirname(createRequire(import.meta.url).resolve('<pkg>/Quiver.yaml'))` is its
27
+ * root, resolved from the caller, whose dependencies are reachable from the
28
+ * caller alone under an isolated `node_modules` layout.
14
29
  *
15
- * Bundler note: importing this entry pulls in `./source-loader.js`,
16
- * `./build.js`, and `./transports/fs-built-transport.js`, all of which
17
- * statically import `node:*` builtins. Browser bundles must never reach this
18
- * module. The main entry (`./index.js`) makes no static or dynamic reference
19
- * to it.
30
+ * Throws `quiver_invalid` on schema violations, `transport_error` on I/O
31
+ * failure.
20
32
  */
21
- import { Quiver as Base } from "./quiver.js";
22
- import { QuiverError } from "./errors.js";
23
- import { scanSourceQuiver, SourceLoader } from "./source-loader.js";
24
- import { buildQuiver } from "./build.js";
25
- import { loadBuiltQuiver } from "./built-loader.js";
26
- import { FsBuiltTransport } from "./transports/fs-built-transport.js";
27
- import { createRequire } from "node:module";
28
- import { dirname } from "node:path";
29
- import { fileURLToPath } from "node:url";
30
- export const Quiver = Base;
31
- // ---------------------------------------------------------------------------
32
- // 2. Runtime patch — install Node-only statics on the shared class.
33
- // ---------------------------------------------------------------------------
34
- Quiver.fromDir = async function fromDir(pathOrFileUrl) {
35
- const dir = pathOrFileUrl.startsWith("file://")
36
- ? fileURLToPath(new URL(".", pathOrFileUrl))
33
+ export async function fromDir(pathOrFileUrl) {
34
+ const dir = pathOrFileUrl.startsWith('file://')
35
+ ? fileURLToPath(new URL('.', pathOrFileUrl))
37
36
  : pathOrFileUrl;
38
37
  const { meta, catalog } = await scanSourceQuiver(dir);
39
- return Base._fromLoader(meta.name, catalog, new SourceLoader(dir));
40
- };
41
- Quiver.fromBuiltDir = async function fromBuiltDir(dirPath) {
38
+ return createQuiver(meta.name, catalog, new SourceLoader(dir));
39
+ }
40
+ /**
41
+ * Loads a packed (build-output) quiver from a local directory containing
42
+ * `latest.json` and the manifest/bundle/store files written by `build`.
43
+ * Symmetric to `Quiver.fromBuiltUrl(url)` but reads from disk instead of HTTP:
44
+ * no network required.
45
+ *
46
+ * Use this for server-side runtime when a packed artifact ships in the
47
+ * deployment image; consumers can keep source quivers as devDependencies and
48
+ * avoid self-fetching over their own load balancer.
49
+ *
50
+ * Throws `quiver_invalid` on format errors, `transport_error` on I/O failure.
51
+ */
52
+ export async function fromBuiltDir(dirPath) {
42
53
  return loadBuiltQuiver(new FsBuiltTransport(dirPath));
43
- };
44
- Quiver.fromPackage = async function fromPackage(specifier) {
45
- const req = createRequire(import.meta.url);
46
- let yamlPath;
47
- try {
48
- yamlPath = req.resolve(`${specifier}/Quiver.yaml`);
49
- }
50
- catch (err) {
51
- throw new QuiverError("transport_error", `Failed to resolve quiver package "${specifier}": ${err.message}`, { cause: err });
52
- }
53
- return Quiver.fromDir(dirname(yamlPath));
54
- };
55
- Quiver.build = async function build(sourceDir, outDir, opts) {
56
- return buildQuiver(sourceDir, outDir, opts);
57
- };
58
- Quiver.buildPackage = async function buildPackage(specifier, outDir, opts) {
59
- const req = createRequire(import.meta.url);
60
- let yamlPath;
61
- try {
62
- yamlPath = req.resolve(`${specifier}/Quiver.yaml`);
63
- }
64
- catch (err) {
65
- throw new QuiverError("transport_error", `Failed to resolve quiver package "${specifier}": ${err.message}`, { cause: err });
66
- }
67
- return buildQuiver(dirname(yamlPath), outDir, opts);
68
- };
69
- // ---------------------------------------------------------------------------
70
- // 3. Re-export the rest of the public surface so consumers get one import.
71
- // ---------------------------------------------------------------------------
72
- export { QuiverError } from "./errors.js";
54
+ }
55
+ /**
56
+ * Reads the Source Quiver at sourceDir, validates it, and writes the runtime
57
+ * build artifact to outDir, which the build clears first and therefore owns.
58
+ *
59
+ * Throws `quiver_invalid` on source validation failures, `transport_error` on
60
+ * I/O failures and on an outDir holding the source quiver or the cwd.
61
+ */
62
+ export async function build(sourceDir, outDir) {
63
+ return buildQuiver(sourceDir, outDir);
64
+ }
65
+ // The rest of the public surface, so a Node consumer needs one import.
66
+ export { Quiver } from './quiver.js';
67
+ export { QuiverError } from './errors.js';
73
68
  // Engine types (`Quillmark`, `Quill`, `Document`, `RenderResult`, …) are not
74
69
  // re-exported: import them straight from the `@quillmark/wasm` peer dependency,
75
70
  // which is the single source of truth for the engine contract.
@@ -5,10 +5,10 @@
5
5
  * simple two-field schema (name, description), but using a proper YAML parser
6
6
  * ensures correct handling of quoting, escaping, and multi-line strings.
7
7
  */
8
- import { parse as parseYaml } from "yaml";
9
- import { QuiverError } from "./errors.js";
8
+ import { parse as parseYaml } from 'yaml';
9
+ import { QuiverError } from './errors.js';
10
10
  const NAME_RE = /^[A-Za-z0-9_-]+$/;
11
- const KNOWN_FIELDS = new Set(["name", "description"]);
11
+ const KNOWN_FIELDS = new Set(['name', 'description']);
12
12
  /**
13
13
  * Parses and validates Quiver.yaml contents.
14
14
  *
@@ -20,44 +20,44 @@ const KNOWN_FIELDS = new Set(["name", "description"]);
20
20
  * - `description` present but not a string
21
21
  */
22
22
  export function parseQuiverYaml(raw) {
23
- const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
23
+ const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
24
24
  let parsed;
25
25
  try {
26
26
  parsed = parseYaml(text);
27
27
  }
28
28
  catch (err) {
29
- throw new QuiverError("quiver_invalid", `Quiver.yaml: YAML parse failure — ${err instanceof Error ? err.message : String(err)}`, { cause: err });
29
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: YAML parse failure — ${err instanceof Error ? err.message : String(err)}`, { cause: err });
30
30
  }
31
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
32
- throw new QuiverError("quiver_invalid", `Quiver.yaml: expected a mapping at the top level, got ${Array.isArray(parsed) ? "array" : String(parsed)}`);
31
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
32
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: expected a mapping at the top level, got ${Array.isArray(parsed) ? 'array' : String(parsed)}`);
33
33
  }
34
34
  const doc = parsed;
35
35
  // Check for unknown fields (strict mode)
36
36
  for (const key of Object.keys(doc)) {
37
37
  if (!KNOWN_FIELDS.has(key)) {
38
- throw new QuiverError("quiver_invalid", `Quiver.yaml: unknown field "${key}" — only "name" and "description" are valid in V1`);
38
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: unknown field "${key}" — only "name" and "description" are valid in V1`);
39
39
  }
40
40
  }
41
41
  // Validate `name`
42
- if (!("name" in doc)) {
43
- throw new QuiverError("quiver_invalid", `Quiver.yaml: required field "name" is missing`);
42
+ if (!('name' in doc)) {
43
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: required field "name" is missing`);
44
44
  }
45
- if (typeof doc["name"] !== "string") {
46
- throw new QuiverError("quiver_invalid", `Quiver.yaml: "name" must be a string, got ${typeof doc["name"]}`);
45
+ if (typeof doc['name'] !== 'string') {
46
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" must be a string, got ${typeof doc['name']}`);
47
47
  }
48
- const name = doc["name"];
48
+ const name = doc['name'];
49
49
  if (!NAME_RE.test(name)) {
50
- throw new QuiverError("quiver_invalid", `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
50
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
51
51
  }
52
52
  // Validate optional `description`
53
- if ("description" in doc && doc["description"] !== undefined) {
54
- if (typeof doc["description"] !== "string") {
55
- throw new QuiverError("quiver_invalid", `Quiver.yaml: "description" must be a string if present, got ${typeof doc["description"]}`);
53
+ if ('description' in doc && doc['description'] !== undefined) {
54
+ if (typeof doc['description'] !== 'string') {
55
+ throw new QuiverError('quiver_invalid', `Quiver.yaml: "description" must be a string if present, got ${typeof doc['description']}`);
56
56
  }
57
57
  }
58
58
  const meta = { name };
59
- if (typeof doc["description"] === "string") {
60
- meta.description = doc["description"];
59
+ if (typeof doc['description'] === 'string') {
60
+ meta.description = doc['description'];
61
61
  }
62
62
  return meta;
63
63
  }
package/dist/quiver.d.ts CHANGED
@@ -5,38 +5,35 @@
5
5
  * (either source-backed or build-output-backed).
6
6
  *
7
7
  * This module is browser-safe: only `fromBuiltUrl` and the instance API live
8
- * here. Node-only factories (`fromDir`, `fromPackage`, `fromBuiltDir`,
9
- * `build`) are installed on this class by `./node.js`, which is the
10
- * consumer's explicit opt-in to the Node API surface.
8
+ * here. The filesystem factories are free functions in `./node.js`; the class
9
+ * is the same either way.
11
10
  */
12
- import { Quill } from "@quillmark/wasm";
13
- /** @internal Internal loader strategy: source or build output. */
11
+ import { Quill } from '@quillmark/wasm';
12
+ /** Loader strategy: source or build output. Package-internal. */
14
13
  export interface QuiverLoader {
15
14
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
16
15
  }
16
+ /**
17
+ * The private constructor's only door, closed over by the static block below.
18
+ * Exported as `createQuiver` for this package's loaders and reachable from no
19
+ * entry in `exports`, so a consumer holds the factories and nothing else.
20
+ */
21
+ declare let create: (name: string, catalog: Map<string, string[]>, loader: QuiverLoader) => Quiver;
17
22
  export declare class Quiver {
18
23
  #private;
19
24
  readonly name: string;
20
25
  /**
21
- * Private constructor — use static factory methods (`Quiver.fromBuiltUrl`,
22
- * or the Node-only `Quiver.fromDir` / `Quiver.fromPackage` /
23
- * `Quiver.fromBuiltDir` installed by `@quillmark/quiver/node`). TS prevents
24
- * external `new Quiver(...)` at compile time.
26
+ * Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
27
+ * or `fromDir` / `fromBuiltDir` from `@quillmark/quiver/node`), which is what
28
+ * names the thing being read.
25
29
  */
26
30
  private constructor();
27
- /**
28
- * @internal Construction escape hatch around the private constructor. Used
29
- * by `loadBuiltQuiver` and by the Node entry (`./node.js`) when installing
30
- * `fromDir` / `fromPackage`. Not part of the public API.
31
- */
32
- static _fromLoader(name: string, catalog: Map<string, string[]>, loader: QuiverLoader): Quiver;
33
31
  /**
34
32
  * Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
35
33
  *
36
34
  * Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
37
35
  * environments. `file://` URLs are rejected — to load build output from
38
- * disk in Node, use `Quiver.fromBuiltDir(path)` from
39
- * `@quillmark/quiver/node`.
36
+ * disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
40
37
  *
41
38
  * Throws `transport_error` on network/HTTP failure, `quiver_invalid`
42
39
  * on format errors.
@@ -55,11 +52,15 @@ export declare class Quiver {
55
52
  * Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
56
53
  * highest matching version in this quiver.
57
54
  *
55
+ * Sync, and in-memory by construction rather than by implementation: every
56
+ * loader materializes the catalog as the quiver is built, and `QuiverLoader`
57
+ * carries one verb, `loadTree`, which resolution never reaches.
58
+ *
58
59
  * Throws:
59
60
  * - `invalid_ref` if ref fails parseQuillRef
60
61
  * - `quill_not_found` if no version matches
61
62
  */
62
- resolve(ref: string): Promise<string>;
63
+ resolve(ref: string): string;
63
64
  /**
64
65
  * Returns a `Quill` for a ref (selector or canonical).
65
66
  *
@@ -73,6 +74,11 @@ export declare class Quiver {
73
74
  * first. Materializes once and caches per canonical ref — concurrent calls
74
75
  * coalesce into a single load.
75
76
  *
77
+ * BORROWED, not handed over: the quill is the quiver's, shared with every
78
+ * other caller for the same ref, and lives as long as the quiver. Do not
79
+ * `free()` it: the next `getQuill` for that ref would hand out a freed
80
+ * handle. A caller wanting one of its own mints it from `.toTree()`.
81
+ *
76
82
  * Throws:
77
83
  * - `invalid_ref` if ref is malformed
78
84
  * - `quill_not_found` if ref does not match any version in this quiver
@@ -80,13 +86,5 @@ export declare class Quiver {
80
86
  * - propagates validation errors from Quill.fromTree() unchanged
81
87
  */
82
88
  getQuill(ref: string): Promise<Quill>;
83
- /**
84
- * Prefetches the tree for every quill version in this quiver. Fail-fast.
85
- *
86
- * Network-bound only — does not materialize Quill instances and does not
87
- * require an engine. Subsequent `getQuill` calls reuse the cached trees,
88
- * skipping the fetch (the tree then evicts as its quill materializes).
89
- * Rejects on the first fetch failure.
90
- */
91
- warm(): Promise<void>;
92
89
  }
90
+ export { create as createQuiver };
package/dist/quiver.js CHANGED
@@ -5,14 +5,19 @@
5
5
  * (either source-backed or build-output-backed).
6
6
  *
7
7
  * This module is browser-safe: only `fromBuiltUrl` and the instance API live
8
- * here. Node-only factories (`fromDir`, `fromPackage`, `fromBuiltDir`,
9
- * `build`) are installed on this class by `./node.js`, which is the
10
- * consumer's explicit opt-in to the Node API surface.
8
+ * here. The filesystem factories are free functions in `./node.js`; the class
9
+ * is the same either way.
11
10
  */
12
- import { QuiverError } from "./errors.js";
13
- import { Quill } from "@quillmark/wasm";
14
- import { parseQuillRef } from "./ref.js";
15
- import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
11
+ import { QuiverError } from './errors.js';
12
+ import { Quill } from '@quillmark/wasm';
13
+ import { parseQuillRef } from './ref.js';
14
+ import { matchesSemverSelector, chooseHighestVersion } from './semver.js';
15
+ /**
16
+ * The private constructor's only door, closed over by the static block below.
17
+ * Exported as `createQuiver` for this package's loaders and reachable from no
18
+ * entry in `exports`, so a consumer holds the factories and nothing else.
19
+ */
20
+ let create;
16
21
  export class Quiver {
17
22
  name;
18
23
  #catalog;
@@ -25,47 +30,34 @@ export class Quiver {
25
30
  */
26
31
  #quillCache = new Map();
27
32
  /**
28
- * Cache of fetched trees, keyed by canonical ref. Populated by `warm()`
29
- * and on first `getQuill` for a ref; an entry is evicted once its quill
30
- * materializes successfully. Promise values so concurrent fetches coalesce.
31
- */
32
- #treeCache = new Map();
33
- /**
34
- * Private constructor — use static factory methods (`Quiver.fromBuiltUrl`,
35
- * or the Node-only `Quiver.fromDir` / `Quiver.fromPackage` /
36
- * `Quiver.fromBuiltDir` installed by `@quillmark/quiver/node`). TS prevents
37
- * external `new Quiver(...)` at compile time.
33
+ * Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
34
+ * or `fromDir` / `fromBuiltDir` from `@quillmark/quiver/node`), which is what
35
+ * names the thing being read.
38
36
  */
39
37
  constructor(name, catalog, loader) {
40
38
  this.name = name;
41
39
  this.#catalog = new Map(catalog);
42
40
  this.#loader = loader;
43
41
  }
44
- /**
45
- * @internal Construction escape hatch around the private constructor. Used
46
- * by `loadBuiltQuiver` and by the Node entry (`./node.js`) when installing
47
- * `fromDir` / `fromPackage`. Not part of the public API.
48
- */
49
- static _fromLoader(name, catalog, loader) {
50
- return new Quiver(name, catalog, loader);
42
+ static {
43
+ create = (name, catalog, loader) => new Quiver(name, catalog, loader);
51
44
  }
52
45
  /**
53
46
  * Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
54
47
  *
55
48
  * Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
56
49
  * environments. `file://` URLs are rejected — to load build output from
57
- * disk in Node, use `Quiver.fromBuiltDir(path)` from
58
- * `@quillmark/quiver/node`.
50
+ * disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
59
51
  *
60
52
  * Throws `transport_error` on network/HTTP failure, `quiver_invalid`
61
53
  * on format errors.
62
54
  */
63
55
  static async fromBuiltUrl(url) {
64
- if (url.startsWith("file://")) {
65
- throw new QuiverError("transport_error", `Quiver.fromBuiltUrl requires an http(s):// or origin-relative URL; got "${url}". For local build output, use Quiver.fromBuiltDir from @quillmark/quiver/node.`);
56
+ if (url.startsWith('file://')) {
57
+ throw new QuiverError('transport_error', `Quiver.fromBuiltUrl requires an http(s):// or origin-relative URL; got "${url}". For local build output, use import { fromBuiltDir } from '@quillmark/quiver/node'.`);
66
58
  }
67
- const { HttpTransport } = await import("./transports/http-transport.js");
68
- const { loadBuiltQuiver } = await import("./built-loader.js");
59
+ const { HttpTransport } = await import('./transports/http-transport.js');
60
+ const { loadBuiltQuiver } = await import('./built-loader.js');
69
61
  const transport = new HttpTransport(url);
70
62
  return loadBuiltQuiver(transport);
71
63
  }
@@ -86,11 +78,15 @@ export class Quiver {
86
78
  * Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
87
79
  * highest matching version in this quiver.
88
80
  *
81
+ * Sync, and in-memory by construction rather than by implementation: every
82
+ * loader materializes the catalog as the quiver is built, and `QuiverLoader`
83
+ * carries one verb, `loadTree`, which resolution never reaches.
84
+ *
89
85
  * Throws:
90
86
  * - `invalid_ref` if ref fails parseQuillRef
91
87
  * - `quill_not_found` if no version matches
92
88
  */
93
- async resolve(ref) {
89
+ resolve(ref) {
94
90
  const parsed = parseQuillRef(ref);
95
91
  const versions = this.#catalog.get(parsed.name);
96
92
  if (versions && versions.length > 0) {
@@ -103,7 +99,7 @@ export class Quiver {
103
99
  return `${parsed.name}@${winner}`;
104
100
  }
105
101
  }
106
- throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
102
+ throw new QuiverError('quill_not_found', `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
107
103
  }
108
104
  /**
109
105
  * Returns a `Quill` for a ref (selector or canonical).
@@ -118,6 +114,11 @@ export class Quiver {
118
114
  * first. Materializes once and caches per canonical ref — concurrent calls
119
115
  * coalesce into a single load.
120
116
  *
117
+ * BORROWED, not handed over: the quill is the quiver's, shared with every
118
+ * other caller for the same ref, and lives as long as the quiver. Do not
119
+ * `free()` it: the next `getQuill` for that ref would hand out a freed
120
+ * handle. A caller wanting one of its own mints it from `.toTree()`.
121
+ *
121
122
  * Throws:
122
123
  * - `invalid_ref` if ref is malformed
123
124
  * - `quill_not_found` if ref does not match any version in this quiver
@@ -125,7 +126,7 @@ export class Quiver {
125
126
  * - propagates validation errors from Quill.fromTree() unchanged
126
127
  */
127
128
  async getQuill(ref) {
128
- const canonicalRef = await this.resolve(ref);
129
+ const canonicalRef = this.resolve(ref);
129
130
  let entry = this.#quillCache.get(canonicalRef);
130
131
  if (entry === undefined) {
131
132
  entry = this.#materializeQuill(canonicalRef).catch((err) => {
@@ -137,70 +138,20 @@ export class Quiver {
137
138
  return entry;
138
139
  }
139
140
  /**
140
- * Internal: load tree (cached) + construct via Quill.fromTree. Errors
141
- * propagate. On success the tree is evicted from `#treeCache` (the quill
142
- * cache now holds the materialized result), so the tree is not retained
143
- * past materialization. On `Quill.fromTree` failure the tree stays cached,
144
- * so a retry skips the refetch.
145
- */
146
- async #materializeQuill(canonicalRef) {
147
- const entry = this.#getTreeCached(canonicalRef);
148
- const tree = await entry;
149
- const quill = Quill.fromTree(tree);
150
- // Evict by identity: only drop the entry if it is still the promise this
151
- // materialization consumed (a concurrent refetch may have replaced it).
152
- if (this.#treeCache.get(canonicalRef) === entry) {
153
- this.#treeCache.delete(canonicalRef);
154
- }
155
- return quill;
156
- }
157
- /**
158
- * Internal: tree cache reader. On miss, fetches via `#loadTree` and stores
159
- * the in-flight Promise. On rejection, evicts so a retry can succeed.
160
- */
161
- #getTreeCached(canonicalRef) {
162
- let entry = this.#treeCache.get(canonicalRef);
163
- if (entry === undefined) {
164
- const at = canonicalRef.indexOf("@");
165
- const name = canonicalRef.slice(0, at);
166
- const version = canonicalRef.slice(at + 1);
167
- entry = this.#loadTree(name, version).catch((err) => {
168
- this.#treeCache.delete(canonicalRef);
169
- throw err;
170
- });
171
- this.#treeCache.set(canonicalRef, entry);
172
- }
173
- return entry;
174
- }
175
- /**
176
- * Internal: validates name/version against the catalog, then delegates to
177
- * the loader. Returns `Map<string, Uint8Array>` suitable for
178
- * `Quill.fromTree(tree)`. Does NOT cache — caching lives in `#getTreeCached`.
141
+ * Internal: load the tree + construct via Quill.fromTree. Errors propagate.
179
142
  *
180
- * Throws `transport_error` if name/version not in catalog or I/O fails.
181
- */
182
- async #loadTree(name, version) {
183
- const versions = this.#catalog.get(name);
184
- if (!versions || !versions.includes(version)) {
185
- throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in quiver "${this.name}"`, { quiverName: this.name, version, ref: `${name}@${version}` });
186
- }
187
- return this.#loader.loadTree(name, version);
188
- }
189
- /**
190
- * Prefetches the tree for every quill version in this quiver. Fail-fast.
143
+ * The tree is held only for the length of this call: the quill cache holds the
144
+ * materialized result, and it is the one cache. A second tree cache beside it
145
+ * would buy a retry after a `Quill.fromTree` throw its refetch, which is a
146
+ * saved round-trip on the path where the quill is broken.
191
147
  *
192
- * Network-bound only — does not materialize Quill instances and does not
193
- * require an engine. Subsequent `getQuill` calls reuse the cached trees,
194
- * skipping the fetch (the tree then evicts as its quill materializes).
195
- * Rejects on the first fetch failure.
148
+ * Every caller arrives through `resolve` or the catalog itself, so the ref is
149
+ * catalog-backed by construction and the loader needs no gate.
196
150
  */
197
- async warm() {
198
- const promises = [];
199
- for (const name of this.quillNames()) {
200
- for (const version of this.versionsOf(name)) {
201
- promises.push(this.#getTreeCached(`${name}@${version}`));
202
- }
203
- }
204
- await Promise.all(promises);
151
+ async #materializeQuill(canonicalRef) {
152
+ const at = canonicalRef.indexOf('@');
153
+ const tree = await this.#loader.loadTree(canonicalRef.slice(0, at), canonicalRef.slice(at + 1));
154
+ return Quill.fromTree(tree);
205
155
  }
206
156
  }
157
+ export { create as createQuiver };
package/dist/ref.d.ts CHANGED
@@ -7,8 +7,8 @@ export interface ParsedQuillRef {
7
7
  selectorDepth?: 1 | 2 | 3;
8
8
  }
9
9
  /**
10
- * Throws QuiverError('invalid_ref') on malformed input.
11
- * Validates name charset: [A-Za-z0-9_-]+
12
- * Validates selector per §5 (x, x.y, x.y.z — digits only, no ranges/operators).
10
+ * Throws QuiverError('invalid_ref') on malformed input. The name is
11
+ * `[A-Za-z0-9_-]+`; the selector is `x`, `x.y` or `x.y.z`, digits only, with no
12
+ * ranges or operators — a quiver resolves a prefix, it does not solve a range.
13
13
  */
14
14
  export declare function parseQuillRef(ref: string): ParsedQuillRef;
package/dist/ref.js CHANGED
@@ -1,25 +1,25 @@
1
- import { QuiverError } from "./errors.js";
1
+ import { QuiverError } from './errors.js';
2
2
  const NAME_RE = /^[A-Za-z0-9_-]+$/;
3
3
  const SELECTOR_RE = /^\d+(\.\d+){0,2}$/;
4
4
  /**
5
- * Throws QuiverError('invalid_ref') on malformed input.
6
- * Validates name charset: [A-Za-z0-9_-]+
7
- * Validates selector per §5 (x, x.y, x.y.z — digits only, no ranges/operators).
5
+ * Throws QuiverError('invalid_ref') on malformed input. The name is
6
+ * `[A-Za-z0-9_-]+`; the selector is `x`, `x.y` or `x.y.z`, digits only, with no
7
+ * ranges or operators — a quiver resolves a prefix, it does not solve a range.
8
8
  */
9
9
  export function parseQuillRef(ref) {
10
10
  if (!ref) {
11
- throw new QuiverError("invalid_ref", `Invalid ref: empty string`, { ref });
11
+ throw new QuiverError('invalid_ref', `Invalid ref: empty string`, { ref });
12
12
  }
13
- const atIndex = ref.indexOf("@");
13
+ const atIndex = ref.indexOf('@');
14
14
  if (atIndex === 0) {
15
15
  // Starts with @, no name
16
- throw new QuiverError("invalid_ref", `Invalid ref: missing name in "${ref}"`, { ref });
16
+ throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
17
17
  }
18
18
  if (atIndex === -1) {
19
19
  // No selector — just a name
20
20
  const name = ref;
21
21
  if (!NAME_RE.test(name)) {
22
- throw new QuiverError("invalid_ref", `Invalid ref: name "${name}" contains invalid characters`, { ref });
22
+ throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
23
23
  }
24
24
  return { name };
25
25
  }
@@ -27,18 +27,20 @@ export function parseQuillRef(ref) {
27
27
  const name = ref.slice(0, atIndex);
28
28
  const selector = ref.slice(atIndex + 1);
29
29
  if (!name) {
30
- throw new QuiverError("invalid_ref", `Invalid ref: missing name in "${ref}"`, { ref });
30
+ throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
31
31
  }
32
32
  if (!selector) {
33
- throw new QuiverError("invalid_ref", `Invalid ref: missing selector after "@" in "${ref}"`, { ref });
33
+ throw new QuiverError('invalid_ref', `Invalid ref: missing selector after "@" in "${ref}"`, {
34
+ ref
35
+ });
34
36
  }
35
37
  if (!NAME_RE.test(name)) {
36
- throw new QuiverError("invalid_ref", `Invalid ref: name "${name}" contains invalid characters`, { ref });
38
+ throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
37
39
  }
38
40
  if (!SELECTOR_RE.test(selector)) {
39
- throw new QuiverError("invalid_ref", `Invalid ref: selector "${selector}" is not a valid semver selector (only x, x.y, x.y.z with digits allowed)`, { ref });
41
+ throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is not a valid semver selector (only x, x.y, x.y.z with digits allowed)`, { ref });
40
42
  }
41
- const parts = selector.split(".");
43
+ const parts = selector.split('.');
42
44
  const depth = parts.length;
43
45
  return { name, selector, selectorDepth: depth };
44
46
  }
package/dist/semver.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  /** Returns true for exactly `x.y.z` with non-negative integer parts. */
2
2
  export declare function isCanonicalSemver(version: string): boolean;
3
- /** Returns true if `version` (canonical) matches `selector` (partial). */
3
+ /**
4
+ * Returns true if `version` (canonical) matches `selector` (partial: `x`,
5
+ * `x.y`, or `x.y.z`). `parseQuillRef` owns selector validation; this is a
6
+ * prefix comparison over an already-valid selector.
7
+ */
4
8
  export declare function matchesSemverSelector(version: string, selector: string): boolean;
5
9
  /** Compares two canonical semver strings. Returns <0, 0, or >0. */
6
10
  export declare function compareSemver(a: string, b: string): number;