@quillmark/quiver 0.12.0 → 0.15.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/quiver.js CHANGED
@@ -10,6 +10,7 @@
10
10
  * consumer's explicit opt-in to the Node API surface.
11
11
  */
12
12
  import { QuiverError } from "./errors.js";
13
+ import { Quill } from "@quillmark/wasm";
13
14
  import { parseQuillRef } from "./ref.js";
14
15
  import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
15
16
  export class Quiver {
@@ -17,15 +18,16 @@ export class Quiver {
17
18
  #catalog;
18
19
  #loader;
19
20
  /**
20
- * Per-engine cache of materialized quills, keyed by canonical ref.
21
- * WeakMap so engines can be GC'd; Promise values so concurrent
22
- * getQuill calls coalesce into a single materialization.
21
+ * Cache of materialized quills, keyed by canonical ref. A `Quill` is now
22
+ * engine-free, portable data (`Quill.fromTree`), so one instance per ref is
23
+ * shared across every engine. Promise values so concurrent getQuill calls
24
+ * coalesce into a single materialization.
23
25
  */
24
- #quillCache = new WeakMap();
26
+ #quillCache = new Map();
25
27
  /**
26
- * Engine-independent cache of fetched trees, keyed by canonical ref.
27
- * Populated by `warm()` and on first `getQuill` for a ref. Promise
28
- * values so concurrent fetches coalesce.
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.
29
31
  */
30
32
  #treeCache = new Map();
31
33
  /**
@@ -78,22 +80,6 @@ export class Quiver {
78
80
  versionsOf(name) {
79
81
  return [...(this.#catalog.get(name) ?? [])];
80
82
  }
81
- /**
82
- * Lazily loads the file tree for a specific quill version.
83
- *
84
- * Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
85
- * Does NOT cache the result — caching of materialized Quill instances
86
- * happens in `getQuill`.
87
- *
88
- * Throws `transport_error` if name/version not in catalog or I/O fails.
89
- */
90
- async loadTree(name, version) {
91
- const versions = this.#catalog.get(name);
92
- if (!versions || !versions.includes(version)) {
93
- throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in quiver "${this.name}"`, { quiverName: this.name, version, ref: `${name}@${version}` });
94
- }
95
- return this.#loader.loadTree(name, version);
96
- }
97
83
  /**
98
84
  * Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
99
85
  *
@@ -120,63 +106,65 @@ export class Quiver {
120
106
  throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
121
107
  }
122
108
  /**
123
- * Returns a render-ready `Quill` for a ref (selector or canonical).
109
+ * Returns a `Quill` for a ref (selector or canonical).
110
+ *
111
+ * The returned quill is materialized from the `@quillmark/wasm` root export
112
+ * — it is engine-free, portable data suitable for schema inspection,
113
+ * validation, blueprint access, and document seeding.
114
+ *
115
+ * A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
124
116
  *
125
- * Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
126
- * form first. Materializes via `engine.quill(tree)` on first call;
127
- * caches per (engine, canonical-ref). Reuses a tree cached by `warm()`
128
- * (or a previous `getQuill`) so the network fetch isn't paid twice.
129
- * Concurrent calls for the same ref coalesce into a single load.
117
+ * Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
118
+ * first. Materializes once and caches per canonical ref — concurrent calls
119
+ * coalesce into a single load.
130
120
  *
131
121
  * Throws:
132
122
  * - `invalid_ref` if ref is malformed
133
123
  * - `quill_not_found` if ref does not match any version in this quiver
134
- * - propagates I/O errors from loadTree unchanged
135
- * - propagates engine errors from engine.quill() unchanged
124
+ * - propagates I/O errors from the loader unchanged
125
+ * - propagates validation errors from Quill.fromTree() unchanged
136
126
  */
137
- async getQuill(ref, opts) {
127
+ async getQuill(ref) {
138
128
  const canonicalRef = await this.resolve(ref);
139
- const engine = opts.engine;
140
- let perEngine = this.#quillCache.get(engine);
141
- if (perEngine === undefined) {
142
- perEngine = new Map();
143
- this.#quillCache.set(engine, perEngine);
144
- }
145
- let entry = perEngine.get(canonicalRef);
129
+ let entry = this.#quillCache.get(canonicalRef);
146
130
  if (entry === undefined) {
147
- entry = this.#materializeQuill(canonicalRef, engine).catch((err) => {
148
- perEngine.delete(canonicalRef);
131
+ entry = this.#materializeQuill(canonicalRef).catch((err) => {
132
+ this.#quillCache.delete(canonicalRef);
149
133
  throw err;
150
134
  });
151
- perEngine.set(canonicalRef, entry);
135
+ this.#quillCache.set(canonicalRef, entry);
152
136
  }
153
137
  return entry;
154
138
  }
155
139
  /**
156
- * Internal: load tree (cached) + invoke engine.quill. Errors propagate.
157
- *
158
- * On success, evicts the tree from the cache so its bytes can be GC'd —
159
- * the materialized Quill is the runtime artifact; the tree is dead weight
160
- * once a Quill exists. On failure, the tree is retained so retries skip
161
- * the network.
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.
162
145
  */
163
- async #materializeQuill(canonicalRef, engine) {
164
- const tree = await this.#getTreeCached(canonicalRef);
165
- const quill = engine.quill(tree);
166
- this.#treeCache.delete(canonicalRef);
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
+ }
167
155
  return quill;
168
156
  }
169
157
  /**
170
- * Internal: tree cache reader. On miss, fetches via `loadTree` and stores
158
+ * Internal: tree cache reader. On miss, fetches via `#loadTree` and stores
171
159
  * the in-flight Promise. On rejection, evicts so a retry can succeed.
172
160
  */
173
- async #getTreeCached(canonicalRef) {
161
+ #getTreeCached(canonicalRef) {
174
162
  let entry = this.#treeCache.get(canonicalRef);
175
163
  if (entry === undefined) {
176
164
  const at = canonicalRef.indexOf("@");
177
165
  const name = canonicalRef.slice(0, at);
178
166
  const version = canonicalRef.slice(at + 1);
179
- entry = this.loadTree(name, version).catch((err) => {
167
+ entry = this.#loadTree(name, version).catch((err) => {
180
168
  this.#treeCache.delete(canonicalRef);
181
169
  throw err;
182
170
  });
@@ -184,12 +172,27 @@ export class Quiver {
184
172
  }
185
173
  return entry;
186
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`.
179
+ *
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
+ }
187
189
  /**
188
190
  * Prefetches the tree for every quill version in this quiver. Fail-fast.
189
191
  *
190
192
  * Network-bound only — does not materialize Quill instances and does not
191
193
  * require an engine. Subsequent `getQuill` calls reuse the cached trees,
192
- * skipping the fetch. Rejects on the first fetch failure.
194
+ * skipping the fetch (the tree then evicts as its quill materializes).
195
+ * Rejects on the first fetch failure.
193
196
  */
194
197
  async warm() {
195
198
  const promises = [];
package/dist/testing.d.ts CHANGED
@@ -7,15 +7,13 @@
7
7
  *
8
8
  * Usage (place this file next to your Quiver.yaml):
9
9
  *
10
- * import { Quillmark, Document } from "@quillmark/wasm";
10
+ * import { Engine } from "@quillmark/wasm";
11
11
  * import { runQuiverTests } from "@quillmark/quiver/testing";
12
- * const engine = new Quillmark();
13
- * runQuiverTests(import.meta.url, engine, Document);
12
+ * runQuiverTests(import.meta.url, new Engine());
14
13
  *
15
14
  * Run with `node --test`.
16
15
  */
17
- import type { QuillmarkLike } from "./engine-types.js";
18
- import type { DocumentFactoryLike } from "./preview.js";
16
+ import type { Engine } from "@quillmark/wasm";
19
17
  /**
20
18
  * Registers a `node:test` describe block that validates every quill
21
19
  * version in the quiver at `metaUrlOrDir` against the provided engine.
@@ -24,7 +22,7 @@ import type { DocumentFactoryLike } from "./preview.js";
24
22
  * to Quiver.yaml). Pass an absolute directory path for any other layout.
25
23
  *
26
24
  * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
27
- * all template files, engine compilation via engine.quill(tree), and a
28
- * full render of each quill's example document.
25
+ * all template files, quill construction via Quill.fromTree(tree), and a
26
+ * full render of each quill's example document via engine.render(quill, doc).
29
27
  */
30
- export declare function runQuiverTests(metaUrlOrDir: string, engine: QuillmarkLike, Document: DocumentFactoryLike): void;
28
+ export declare function runQuiverTests(metaUrlOrDir: string, engine: Engine): void;
package/dist/testing.js CHANGED
@@ -7,10 +7,9 @@
7
7
  *
8
8
  * Usage (place this file next to your Quiver.yaml):
9
9
  *
10
- * import { Quillmark, Document } from "@quillmark/wasm";
10
+ * import { Engine } from "@quillmark/wasm";
11
11
  * import { runQuiverTests } from "@quillmark/quiver/testing";
12
- * const engine = new Quillmark();
13
- * runQuiverTests(import.meta.url, engine, Document);
12
+ * runQuiverTests(import.meta.url, new Engine());
14
13
  *
15
14
  * Run with `node --test`.
16
15
  */
@@ -27,10 +26,10 @@ import { Quiver } from "./node.js";
27
26
  * to Quiver.yaml). Pass an absolute directory path for any other layout.
28
27
  *
29
28
  * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
30
- * all template files, engine compilation via engine.quill(tree), and a
31
- * full render of each quill's example document.
29
+ * all template files, quill construction via Quill.fromTree(tree), and a
30
+ * full render of each quill's example document via engine.render(quill, doc).
32
31
  */
33
- export function runQuiverTests(metaUrlOrDir, engine, Document) {
32
+ export function runQuiverTests(metaUrlOrDir, engine) {
34
33
  describe("Quiver", () => {
35
34
  let quiver;
36
35
  before(async () => {
@@ -45,9 +44,18 @@ export function runQuiverTests(metaUrlOrDir, engine, Document) {
45
44
  for (const name of quiver.quillNames()) {
46
45
  for (const version of quiver.versionsOf(name)) {
47
46
  const ref = `${name}@${version}`;
48
- const quill = await quiver.getQuill(ref, { engine });
49
- const doc = Document.fromMarkdown(quill.example);
50
- const result = quill.render(doc);
47
+ // The Engine takes the core Quill + its seeded Document directly and
48
+ // clones them into the backend; the doc clone is freed inside render,
49
+ // and we free our own core Document here.
50
+ const quill = await quiver.getQuill(ref);
51
+ const doc = quill.seedDocument();
52
+ let result;
53
+ try {
54
+ result = await engine.render(quill, doc);
55
+ }
56
+ finally {
57
+ doc.free();
58
+ }
51
59
  if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
52
60
  throw new Error(`${ref}: example render produced no artifacts`);
53
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.12.0",
3
+ "version": "0.15.0",
4
4
  "description": "Quiver registry and build tooling for Quillmark",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "author": "Quillmark Contributors",
22
22
  "engines": {
23
- "node": ">=24"
23
+ "node": ">=22"
24
24
  },
25
25
  "exports": {
26
26
  ".": {
@@ -47,18 +47,17 @@
47
47
  ],
48
48
  "files": [
49
49
  "dist",
50
- "PROGRAM.md",
51
50
  "README.md"
52
51
  ],
53
52
  "peerDependencies": {
54
- "@quillmark/wasm": ">=0.86.0"
53
+ "@quillmark/wasm": ">=0.90.0-0"
55
54
  },
56
55
  "dependencies": {
57
56
  "fflate": "^0.8.2",
58
57
  "yaml": "^2.8.3"
59
58
  },
60
59
  "devDependencies": {
61
- "@quillmark/wasm": "^0.86.0",
60
+ "@quillmark/wasm": "0.90.0",
62
61
  "@types/node": "^25.3.3",
63
62
  "typescript": "^5.9.3",
64
63
  "vitest": "^4.0.18"
@@ -68,6 +67,7 @@
68
67
  },
69
68
  "scripts": {
70
69
  "build": "tsc",
70
+ "prepack": "rm -rf dist && tsc",
71
71
  "test": "vitest run",
72
72
  "lint": "tsc --noEmit"
73
73
  }