@quillmark/quiver 0.3.0 → 0.4.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/PROGRAM.md +49 -52
- package/README.md +28 -23
- package/dist/engine-types.d.ts +1 -1
- package/dist/engine-types.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/quiver.d.ts +40 -1
- package/dist/quiver.js +122 -1
- package/dist/testing.js +1 -4
- package/package.json +1 -1
- package/dist/registry.d.ts +0 -39
- package/dist/registry.js +0 -115
package/PROGRAM.md
CHANGED
|
@@ -83,27 +83,15 @@ Unknown fields in `Quiver.yaml` are a **validation error** (`quiver_invalid`). S
|
|
|
83
83
|
- "Transport" is not a first-class concept; HTTP fetching is an internal
|
|
84
84
|
detail of `fromBuilt`
|
|
85
85
|
|
|
86
|
-
### 4)
|
|
86
|
+
### 4) Single-Quiver Scope (V1)
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
V1 has no multi-quiver composition layer. Each `Quiver` instance is
|
|
89
|
+
independent: consumers load one quiver and call `resolve` / `getQuill` /
|
|
90
|
+
`warm` directly on it. There is no `QuiverRegistry`.
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
- **Precedence is a hard filter**
|
|
95
|
-
- Scan quivers in order
|
|
96
|
-
- First quiver with any matching candidate wins
|
|
97
|
-
- Choose highest matching version **within that quiver**
|
|
98
|
-
|
|
99
|
-
Applies to:
|
|
100
|
-
|
|
101
|
-
- unqualified refs (e.g. `usaf_memo`)
|
|
102
|
-
- selector refs (e.g. `usaf_memo@1.2`)
|
|
103
|
-
|
|
104
|
-
No global highest-across-all-quivers behavior.
|
|
105
|
-
|
|
106
|
-
**Identity collision:** duplicate `Quiver.yaml.name` across composed quivers is an error in V1.
|
|
92
|
+
If composition becomes a real use case, the additive path is a thin
|
|
93
|
+
`Quiver.compose([a, b, ...])` factory that returns a quiver-shaped
|
|
94
|
+
composite — it is intentionally out of scope for V1.
|
|
107
95
|
|
|
108
96
|
### 5) Semver Selector Rules Are Strict and Small
|
|
109
97
|
|
|
@@ -129,20 +117,35 @@ Canonical version format:
|
|
|
129
117
|
|
|
130
118
|
Canonicalization:
|
|
131
119
|
|
|
132
|
-
- resolve selector to canonical ref once per
|
|
133
|
-
- key
|
|
120
|
+
- resolve selector to canonical ref once per call to `getQuill`
|
|
121
|
+
- key the tree cache by canonical ref; key the quill cache by (engine, canonical-ref)
|
|
134
122
|
|
|
135
123
|
### 6) Warm/Prefetch Is Purely a Quiver Concern
|
|
136
124
|
|
|
137
|
-
`warm()`
|
|
125
|
+
`warm()` is the network prefetch step. It fetches every quill's tree and
|
|
126
|
+
populates the per-quiver tree cache. It does **not** materialize Quill
|
|
127
|
+
instances and does **not** require an engine — `engine.quill(tree)` is
|
|
128
|
+
microseconds and runs lazily inside `getQuill`. A subsequent `getQuill`
|
|
129
|
+
reuses the cached tree, so the network fetch isn't paid twice.
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
- `resolve()` must work even if nothing is warmed
|
|
131
|
+
Tree cache lifecycle:
|
|
141
132
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
133
|
+
- `warm()` populates the tree cache.
|
|
134
|
+
- First `getQuill(ref, { engine })` reads the tree, materializes the
|
|
135
|
+
Quill via `engine.quill(tree)`, then evicts the tree so its bytes can
|
|
136
|
+
be GC'd. The materialized Quill is what's kept (per engine).
|
|
137
|
+
- If `engine.quill` throws, the tree is retained so a retry skips the
|
|
138
|
+
network.
|
|
139
|
+
- Repeated `getQuill` on the same engine hits the per-engine quill cache
|
|
140
|
+
— no tree access at all.
|
|
141
|
+
- A subsequent `getQuill` for a different engine refetches the tree
|
|
142
|
+
(single-engine apps never pay this cost).
|
|
143
|
+
|
|
144
|
+
Other invariants:
|
|
145
|
+
|
|
146
|
+
- `resolve()` works whether or not anything is warmed.
|
|
147
|
+
- Warm semantics are identical for source-loaded and built-output-loaded
|
|
148
|
+
quivers; the loader hides the difference.
|
|
146
149
|
|
|
147
150
|
### 7) Engine Boundary: New Canonical Contract (Node / JS–WASM only)
|
|
148
151
|
|
|
@@ -164,7 +167,7 @@ For advanced dynamic-asset behavior, defer to Quillmark’s JS/WASM docs; the de
|
|
|
164
167
|
### 8) Markdown and Ref Parsing Boundary
|
|
165
168
|
|
|
166
169
|
- Markdown parsing does not require a quill registry: `Document.fromMarkdown(markdown)`
|
|
167
|
-
- Quiver owns ref parsing and selector resolution for its own API (`resolve`, `warm`, validation)
|
|
170
|
+
- Quiver owns ref parsing and selector resolution for its own API (`resolve`, `getQuill`, `warm`, validation)
|
|
168
171
|
- QUILL field is informational at render time; Quiver routes to the intended quill explicitly without mutating the parsed document in V1
|
|
169
172
|
|
|
170
173
|
Upstream behavior note:
|
|
@@ -254,13 +257,12 @@ V1 code catalog (closed set):
|
|
|
254
257
|
| Code | Fires when |
|
|
255
258
|
|---|---|
|
|
256
259
|
| `invalid_ref` | Malformed ref string at `resolve()`/`warm()` boundary (fails `parseQuillRef`) |
|
|
257
|
-
| `quill_not_found` | Selector did not match any quill in
|
|
260
|
+
| `quill_not_found` | Selector did not match any quill in the quiver |
|
|
258
261
|
| `quiver_invalid` | `Quiver.yaml` or hashed manifest malformed, unknown field, non-canonical version on disk, or font/bundle hash mismatch |
|
|
259
262
|
| `transport_error` | I/O failure: missing path, HTTP non-2xx, network error, permission error. Wraps underlying cause. |
|
|
260
|
-
| `quiver_collision` | Two composed quivers share `Quiver.yaml.name` at registry construction |
|
|
261
263
|
|
|
262
264
|
Notes:
|
|
263
|
-
- `quill_not_found` is selector-resolution failure
|
|
265
|
+
- `quill_not_found` is selector-resolution failure within a quiver's catalog.
|
|
264
266
|
- `transport_error` is artifact access failure (filesystem/HTTP/network/permissions), including missing packed files and HTTP 404.
|
|
265
267
|
- Legacy categories such as `manifest_invalid`, `quill_load_failed`, and `backend_not_found` are folded into `quiver_invalid` or `transport_error` in V1.
|
|
266
268
|
|
|
@@ -386,43 +388,38 @@ class Quiver {
|
|
|
386
388
|
|
|
387
389
|
readonly name: string; // from Quiver.yaml
|
|
388
390
|
|
|
389
|
-
// Read-only introspection and lazy tree access used by
|
|
390
|
-
//
|
|
391
|
+
// Read-only introspection and lazy tree access; also used internally by
|
|
392
|
+
// resolve/getQuill/warm.
|
|
391
393
|
quillNames(): string[]; // sorted lex
|
|
392
394
|
versionsOf(name: string): string[]; // sorted desc
|
|
393
395
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
394
|
-
}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
```ts
|
|
398
|
-
class QuiverRegistry {
|
|
399
|
-
constructor(args: { engine: Quillmark; quivers: Quiver[] });
|
|
400
396
|
|
|
401
397
|
// Selector ref -> canonical ref. Throws invalid_ref / quill_not_found.
|
|
402
398
|
resolve(ref: string): Promise<string>;
|
|
403
399
|
|
|
404
|
-
//
|
|
405
|
-
|
|
400
|
+
// Selector or canonical ref -> render-ready quill handle (materialized via
|
|
401
|
+
// engine.quill(tree), cached per (engine, canonical-ref)).
|
|
402
|
+
getQuill(ref: string, opts: { engine: Quillmark }): Promise<Quill>;
|
|
406
403
|
|
|
407
|
-
//
|
|
404
|
+
// Prefetches every quill tree (network-only; engine not required).
|
|
405
|
+
// Subsequent getQuill calls reuse the cached tree. Fail-fast.
|
|
408
406
|
warm(): Promise<void>;
|
|
409
407
|
}
|
|
410
408
|
|
|
411
409
|
class QuiverError extends Error {
|
|
412
|
-
code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error"
|
|
410
|
+
code: "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
|
|
413
411
|
// plus contextual payload fields
|
|
414
412
|
}
|
|
415
413
|
```
|
|
416
414
|
|
|
417
|
-
**No render wrapper.** Callers invoke `quill.render(doc, opts?)` (and `quill.open(doc)` when needed) after `
|
|
415
|
+
**No render wrapper.** Callers invoke `quill.render(doc, opts?)` (and `quill.open(doc)` when needed) after `getQuill()`. Quiver never mirrors Quillmark render APIs.
|
|
418
416
|
|
|
419
417
|
**Internal (not exported):** `QuiverManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state, source-vs-built layout detection.
|
|
420
418
|
|
|
421
419
|
Hot-path flow:
|
|
422
420
|
```ts
|
|
423
421
|
const doc = Document.fromMarkdown(md);
|
|
424
|
-
const
|
|
425
|
-
const quill = await registry.getQuill(canonicalRef);
|
|
422
|
+
const quill = await quiver.getQuill(doc.quillRef, { engine });
|
|
426
423
|
const result = quill.render(doc, { format: "pdf" });
|
|
427
424
|
```
|
|
428
425
|
|
|
@@ -433,8 +430,8 @@ const result = quill.render(doc, { format: "pdf" });
|
|
|
433
430
|
**Entrypoints:**
|
|
434
431
|
- `@quillmark/quiver` (main, browser-safe): `Quiver` class with only
|
|
435
432
|
`fromBuilt` functional (Node-only loaders/builder throw
|
|
436
|
-
`transport_error` if reached in browser), `
|
|
437
|
-
`
|
|
433
|
+
`transport_error` if reached in browser), `QuiverError`,
|
|
434
|
+
`QuillmarkLike`, `QuillLike`, shared types.
|
|
438
435
|
- `@quillmark/quiver/node`: adds `Quiver.fromPackage`, `Quiver.fromDir`,
|
|
439
436
|
`Quiver.build` behaviors. Single `Quiver` class — Node-only factories
|
|
440
437
|
fail fast outside Node.
|
|
@@ -461,7 +458,7 @@ const result = quill.render(doc, { format: "pdf" });
|
|
|
461
458
|
- inter-quiver dependency graph in `Quiver.yaml`
|
|
462
459
|
- marketplace/discovery service
|
|
463
460
|
- advanced warm strategies beyond API-compatible hooks
|
|
464
|
-
- multi-quiver
|
|
461
|
+
- multi-quiver composition (single quiver per consumer in V1)
|
|
465
462
|
|
|
466
463
|
---
|
|
467
464
|
|
|
@@ -472,11 +469,11 @@ All V1 planner questions resolved; implementation plan can proceed against the s
|
|
|
472
469
|
1. ~~Final `Quiver` interface shape and transport factoring style~~ → Single `Quiver` class, three loaders (`fromPackage`, `fromDir`, `fromBuilt`) + one builder (`build`). Each loader names what it loads; no auto-detection.
|
|
473
470
|
2. ~~Final `Quiver.yaml` schema and unknown-field policy~~ → See §2: alphanumeric `name` and optional tooling-only `description`. Unknown fields are `quiver_invalid`.
|
|
474
471
|
3. ~~Canonical ref grammar and parser API contract~~ → Internal `parseQuillRef`, not exported. Selector syntax per §5. Throws `invalid_ref`.
|
|
475
|
-
4. ~~Exact warning policy for shadowed refs across quivers~~ →
|
|
472
|
+
4. ~~Exact warning policy for shadowed refs across quivers~~ → N/A in V1: no multi-quiver composition layer; each `Quiver` instance is independent (§4).
|
|
476
473
|
5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `build()`.
|
|
477
474
|
6. ~~Build output directory structure~~ → See "Runtime Artifact Format (normative)".
|
|
478
475
|
7. ~~Node/browser entrypoint split~~ → See "Package Structure": main + `/node` subpath, single `Quiver` class.
|
|
479
|
-
8. ~~Final exported type names~~ → `Quiver`, `
|
|
476
|
+
8. ~~Final exported type names~~ → `Quiver`, `QuiverError`. Hot-path entry is `Quiver.getQuill(ref, { engine })`.
|
|
480
477
|
|
|
481
478
|
---
|
|
482
479
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @quillmark/quiver
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Load and build collections of quills for rendering with `@quillmark/wasm`.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -51,19 +51,27 @@ the main API instead.
|
|
|
51
51
|
|
|
52
52
|
```ts
|
|
53
53
|
import { Quillmark, Document } from "@quillmark/wasm";
|
|
54
|
-
import { Quiver
|
|
55
|
-
|
|
56
|
-
const quiver = await Quiver.fromPackage("@org/my-quiver");
|
|
54
|
+
import { Quiver } from "@quillmark/quiver/node";
|
|
57
55
|
|
|
58
56
|
const engine = new Quillmark();
|
|
59
|
-
const
|
|
57
|
+
const quiver = await Quiver.fromPackage("@org/my-quiver");
|
|
60
58
|
|
|
61
59
|
const doc = Document.fromMarkdown(markdownString);
|
|
62
|
-
const
|
|
63
|
-
const quill = await registry.getQuill(canonicalRef);
|
|
60
|
+
const quill = await quiver.getQuill(doc.quillRef, { engine });
|
|
64
61
|
const result = quill.render(doc, { format: "pdf" });
|
|
65
62
|
```
|
|
66
63
|
|
|
64
|
+
`getQuill` accepts both selector refs (`"memo"`, `"memo@1"`) and canonical
|
|
65
|
+
refs (`"memo@1.0.0"`). It resolves the selector, materializes the quill via
|
|
66
|
+
`engine.quill(tree)`, and caches per (engine, canonical-ref). Concurrent
|
|
67
|
+
calls for the same ref share a single load.
|
|
68
|
+
|
|
69
|
+
If you only need the canonical ref (without materializing), use `resolve`:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const canonicalRef = await quiver.resolve("memo"); // "memo@1.1.0"
|
|
73
|
+
```
|
|
74
|
+
|
|
67
75
|
## Consuming a quiver (browser)
|
|
68
76
|
|
|
69
77
|
Browsers cannot read the source layout directly, so build at deploy time and
|
|
@@ -81,10 +89,10 @@ await Quiver.build(
|
|
|
81
89
|
|
|
82
90
|
```ts
|
|
83
91
|
// browser runtime
|
|
84
|
-
import { Quiver
|
|
92
|
+
import { Quiver } from "@quillmark/quiver";
|
|
85
93
|
|
|
86
94
|
const quiver = await Quiver.fromBuilt("/quivers/my-quiver/");
|
|
87
|
-
const
|
|
95
|
+
const quill = await quiver.getQuill(doc.quillRef, { engine });
|
|
88
96
|
```
|
|
89
97
|
|
|
90
98
|
## Advanced: pre-built distribution to a CDN
|
|
@@ -101,23 +109,20 @@ await Quiver.build("./my-quiver", "./dist/my-quiver");
|
|
|
101
109
|
const quiver = await Quiver.fromBuilt("https://cdn.example.com/quivers/my-quiver/");
|
|
102
110
|
```
|
|
103
111
|
|
|
104
|
-
## Warm (prefetch all
|
|
112
|
+
## Warm (prefetch all quill trees)
|
|
105
113
|
|
|
106
114
|
```ts
|
|
107
|
-
await
|
|
115
|
+
await quiver.warm();
|
|
108
116
|
```
|
|
109
117
|
|
|
110
|
-
|
|
118
|
+
`warm()` is network-only: it fetches every quill's tree and caches them.
|
|
119
|
+
It does not require an engine and does not materialize Quill instances —
|
|
120
|
+
that happens lazily on the first `getQuill` call, which is microseconds.
|
|
121
|
+
A subsequent `getQuill` reuses the cached tree, skipping the fetch.
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
const registry = new QuiverRegistry({
|
|
117
|
-
engine,
|
|
118
|
-
quivers: [primaryQuiver, fallbackQuiver],
|
|
119
|
-
});
|
|
120
|
-
```
|
|
123
|
+
Once a tree has been turned into a Quill, the cached tree is dropped so
|
|
124
|
+
its bytes can be GC'd — the materialized Quill is the runtime artifact.
|
|
125
|
+
Calling `warm()` again refills the tree cache.
|
|
121
126
|
|
|
122
127
|
## Error handling
|
|
123
128
|
|
|
@@ -127,7 +132,7 @@ All errors are instances of `QuiverError` with a `code` field.
|
|
|
127
132
|
import { QuiverError } from "@quillmark/quiver";
|
|
128
133
|
|
|
129
134
|
try {
|
|
130
|
-
|
|
135
|
+
await quiver.resolve("unknown_quill");
|
|
131
136
|
} catch (err) {
|
|
132
137
|
if (err instanceof QuiverError) {
|
|
133
138
|
console.error(err.code); // e.g. "quill_not_found"
|
|
@@ -137,7 +142,7 @@ try {
|
|
|
137
142
|
}
|
|
138
143
|
```
|
|
139
144
|
|
|
140
|
-
Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_error
|
|
145
|
+
Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_error`.
|
|
141
146
|
|
|
142
147
|
## Full specification
|
|
143
148
|
|
package/dist/engine-types.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* test doubles) satisfy the contract structurally.
|
|
12
12
|
*
|
|
13
13
|
* These types are INTERNAL — never re-exported from index.ts. They exist so
|
|
14
|
-
*
|
|
14
|
+
* quiver.ts never imports from @quillmark/wasm directly and so test doubles
|
|
15
15
|
* can satisfy the contract without pulling the real WASM module.
|
|
16
16
|
*
|
|
17
17
|
* Call-site note: Quiver never invokes `render` or `open` itself; consumers do
|
package/dist/engine-types.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* test doubles) satisfy the contract structurally.
|
|
12
12
|
*
|
|
13
13
|
* These types are INTERNAL — never re-exported from index.ts. They exist so
|
|
14
|
-
*
|
|
14
|
+
* quiver.ts never imports from @quillmark/wasm directly and so test doubles
|
|
15
15
|
* can satisfy the contract without pulling the real WASM module.
|
|
16
16
|
*
|
|
17
17
|
* Call-site note: Quiver never invokes `render` or `open` itself; consumers do
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type QuiverErrorCode = "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error"
|
|
1
|
+
export type QuiverErrorCode = "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error";
|
|
2
2
|
export declare class QuiverError extends Error {
|
|
3
3
|
readonly code: QuiverErrorCode;
|
|
4
4
|
/** Offending ref string, when available. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { QuiverError } from "./errors.js";
|
|
2
2
|
export type { QuiverErrorCode } from "./errors.js";
|
|
3
3
|
export { Quiver } from "./quiver.js";
|
|
4
|
-
export { QuiverRegistry } from "./registry.js";
|
|
5
4
|
export type { BuildOptions } from "./build.js";
|
|
6
5
|
export type { QuillmarkLike, QuillLike } from "./engine-types.js";
|
package/dist/index.js
CHANGED
package/dist/quiver.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* (either source-backed or build-output-backed).
|
|
6
6
|
*/
|
|
7
7
|
import type { BuildOptions } from "./build.js";
|
|
8
|
+
import type { QuillmarkLike, QuillLike } from "./engine-types.js";
|
|
8
9
|
/** @internal Internal loader strategy: source or build output. */
|
|
9
10
|
export interface QuiverLoader {
|
|
10
11
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
@@ -74,9 +75,47 @@ export declare class Quiver {
|
|
|
74
75
|
* Lazily loads the file tree for a specific quill version.
|
|
75
76
|
*
|
|
76
77
|
* Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
|
|
77
|
-
* Does NOT cache the result — caching
|
|
78
|
+
* Does NOT cache the result — caching of materialized Quill instances
|
|
79
|
+
* happens in `getQuill`.
|
|
78
80
|
*
|
|
79
81
|
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
80
82
|
*/
|
|
81
83
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
84
|
+
/**
|
|
85
|
+
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
86
|
+
*
|
|
87
|
+
* Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
|
|
88
|
+
* highest matching version in this quiver.
|
|
89
|
+
*
|
|
90
|
+
* Throws:
|
|
91
|
+
* - `invalid_ref` if ref fails parseQuillRef
|
|
92
|
+
* - `quill_not_found` if no version matches
|
|
93
|
+
*/
|
|
94
|
+
resolve(ref: string): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Returns a render-ready `Quill` for a ref (selector or canonical).
|
|
97
|
+
*
|
|
98
|
+
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
|
|
99
|
+
* form first. Materializes via `engine.quill(tree)` on first call;
|
|
100
|
+
* caches per (engine, canonical-ref). Reuses a tree cached by `warm()`
|
|
101
|
+
* (or a previous `getQuill`) so the network fetch isn't paid twice.
|
|
102
|
+
* Concurrent calls for the same ref coalesce into a single load.
|
|
103
|
+
*
|
|
104
|
+
* Throws:
|
|
105
|
+
* - `invalid_ref` if ref is malformed
|
|
106
|
+
* - `quill_not_found` if ref does not match any version in this quiver
|
|
107
|
+
* - propagates I/O errors from loadTree unchanged
|
|
108
|
+
* - propagates engine errors from engine.quill() unchanged
|
|
109
|
+
*/
|
|
110
|
+
getQuill(ref: string, opts: {
|
|
111
|
+
engine: QuillmarkLike;
|
|
112
|
+
}): Promise<QuillLike>;
|
|
113
|
+
/**
|
|
114
|
+
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
115
|
+
*
|
|
116
|
+
* Network-bound only — does not materialize Quill instances and does not
|
|
117
|
+
* require an engine. Subsequent `getQuill` calls reuse the cached trees,
|
|
118
|
+
* skipping the fetch. Rejects on the first fetch failure.
|
|
119
|
+
*/
|
|
120
|
+
warm(): Promise<void>;
|
|
82
121
|
}
|
package/dist/quiver.js
CHANGED
|
@@ -6,10 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { QuiverError } from "./errors.js";
|
|
8
8
|
import { assertNode } from "./assert-node.js";
|
|
9
|
+
import { parseQuillRef } from "./ref.js";
|
|
10
|
+
import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
|
|
9
11
|
export class Quiver {
|
|
10
12
|
name;
|
|
11
13
|
#catalog;
|
|
12
14
|
#loader;
|
|
15
|
+
/**
|
|
16
|
+
* Per-engine cache of materialized quills, keyed by canonical ref.
|
|
17
|
+
* WeakMap so engines can be GC'd; Promise values so concurrent
|
|
18
|
+
* getQuill calls coalesce into a single materialization.
|
|
19
|
+
*/
|
|
20
|
+
#quillCache = new WeakMap();
|
|
21
|
+
/**
|
|
22
|
+
* Engine-independent cache of fetched trees, keyed by canonical ref.
|
|
23
|
+
* Populated by `warm()` and on first `getQuill` for a ref. Promise
|
|
24
|
+
* values so concurrent fetches coalesce.
|
|
25
|
+
*/
|
|
26
|
+
#treeCache = new Map();
|
|
13
27
|
/**
|
|
14
28
|
* Private constructor — use static factory methods.
|
|
15
29
|
* TS prevents external `new Quiver(...)` at compile time.
|
|
@@ -118,7 +132,8 @@ export class Quiver {
|
|
|
118
132
|
* Lazily loads the file tree for a specific quill version.
|
|
119
133
|
*
|
|
120
134
|
* Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
|
|
121
|
-
* Does NOT cache the result — caching
|
|
135
|
+
* Does NOT cache the result — caching of materialized Quill instances
|
|
136
|
+
* happens in `getQuill`.
|
|
122
137
|
*
|
|
123
138
|
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
124
139
|
*/
|
|
@@ -129,4 +144,110 @@ export class Quiver {
|
|
|
129
144
|
}
|
|
130
145
|
return this.#loader.loadTree(name, version);
|
|
131
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
149
|
+
*
|
|
150
|
+
* Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
|
|
151
|
+
* highest matching version in this quiver.
|
|
152
|
+
*
|
|
153
|
+
* Throws:
|
|
154
|
+
* - `invalid_ref` if ref fails parseQuillRef
|
|
155
|
+
* - `quill_not_found` if no version matches
|
|
156
|
+
*/
|
|
157
|
+
async resolve(ref) {
|
|
158
|
+
const parsed = parseQuillRef(ref);
|
|
159
|
+
const versions = this.#catalog.get(parsed.name);
|
|
160
|
+
if (versions && versions.length > 0) {
|
|
161
|
+
const candidates = parsed.selector === undefined
|
|
162
|
+
? [...versions]
|
|
163
|
+
: versions.filter((v) => matchesSemverSelector(v, parsed.selector));
|
|
164
|
+
if (candidates.length > 0) {
|
|
165
|
+
// chooseHighestVersion returns null only for empty arrays; candidates is non-empty.
|
|
166
|
+
const winner = chooseHighestVersion(candidates);
|
|
167
|
+
return `${parsed.name}@${winner}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns a render-ready `Quill` for a ref (selector or canonical).
|
|
174
|
+
*
|
|
175
|
+
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
|
|
176
|
+
* form first. Materializes via `engine.quill(tree)` on first call;
|
|
177
|
+
* caches per (engine, canonical-ref). Reuses a tree cached by `warm()`
|
|
178
|
+
* (or a previous `getQuill`) so the network fetch isn't paid twice.
|
|
179
|
+
* Concurrent calls for the same ref coalesce into a single load.
|
|
180
|
+
*
|
|
181
|
+
* Throws:
|
|
182
|
+
* - `invalid_ref` if ref is malformed
|
|
183
|
+
* - `quill_not_found` if ref does not match any version in this quiver
|
|
184
|
+
* - propagates I/O errors from loadTree unchanged
|
|
185
|
+
* - propagates engine errors from engine.quill() unchanged
|
|
186
|
+
*/
|
|
187
|
+
async getQuill(ref, opts) {
|
|
188
|
+
const canonicalRef = await this.resolve(ref);
|
|
189
|
+
const engine = opts.engine;
|
|
190
|
+
let perEngine = this.#quillCache.get(engine);
|
|
191
|
+
if (perEngine === undefined) {
|
|
192
|
+
perEngine = new Map();
|
|
193
|
+
this.#quillCache.set(engine, perEngine);
|
|
194
|
+
}
|
|
195
|
+
let entry = perEngine.get(canonicalRef);
|
|
196
|
+
if (entry === undefined) {
|
|
197
|
+
entry = this.#materializeQuill(canonicalRef, engine).catch((err) => {
|
|
198
|
+
perEngine.delete(canonicalRef);
|
|
199
|
+
throw err;
|
|
200
|
+
});
|
|
201
|
+
perEngine.set(canonicalRef, entry);
|
|
202
|
+
}
|
|
203
|
+
return entry;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Internal: load tree (cached) + invoke engine.quill. Errors propagate.
|
|
207
|
+
*
|
|
208
|
+
* On success, evicts the tree from the cache so its bytes can be GC'd —
|
|
209
|
+
* the materialized Quill is the runtime artifact; the tree is dead weight
|
|
210
|
+
* once a Quill exists. On failure, the tree is retained so retries skip
|
|
211
|
+
* the network.
|
|
212
|
+
*/
|
|
213
|
+
async #materializeQuill(canonicalRef, engine) {
|
|
214
|
+
const tree = await this.#getTreeCached(canonicalRef);
|
|
215
|
+
const quill = engine.quill(tree);
|
|
216
|
+
this.#treeCache.delete(canonicalRef);
|
|
217
|
+
return quill;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Internal: tree cache reader. On miss, fetches via `loadTree` and stores
|
|
221
|
+
* the in-flight Promise. On rejection, evicts so a retry can succeed.
|
|
222
|
+
*/
|
|
223
|
+
async #getTreeCached(canonicalRef) {
|
|
224
|
+
let entry = this.#treeCache.get(canonicalRef);
|
|
225
|
+
if (entry === undefined) {
|
|
226
|
+
const at = canonicalRef.indexOf("@");
|
|
227
|
+
const name = canonicalRef.slice(0, at);
|
|
228
|
+
const version = canonicalRef.slice(at + 1);
|
|
229
|
+
entry = this.loadTree(name, version).catch((err) => {
|
|
230
|
+
this.#treeCache.delete(canonicalRef);
|
|
231
|
+
throw err;
|
|
232
|
+
});
|
|
233
|
+
this.#treeCache.set(canonicalRef, entry);
|
|
234
|
+
}
|
|
235
|
+
return entry;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
239
|
+
*
|
|
240
|
+
* Network-bound only — does not materialize Quill instances and does not
|
|
241
|
+
* require an engine. Subsequent `getQuill` calls reuse the cached trees,
|
|
242
|
+
* skipping the fetch. Rejects on the first fetch failure.
|
|
243
|
+
*/
|
|
244
|
+
async warm() {
|
|
245
|
+
const promises = [];
|
|
246
|
+
for (const name of this.quillNames()) {
|
|
247
|
+
for (const version of this.versionsOf(name)) {
|
|
248
|
+
promises.push(this.#getTreeCached(`${name}@${version}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await Promise.all(promises);
|
|
252
|
+
}
|
|
132
253
|
}
|
package/dist/testing.js
CHANGED
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { describe, it, before } from "node:test";
|
|
18
18
|
import { Quiver } from "./quiver.js";
|
|
19
|
-
import { QuiverRegistry } from "./registry.js";
|
|
20
19
|
/**
|
|
21
20
|
* Registers a `node:test` describe block that validates every quill
|
|
22
21
|
* version in the quiver at `metaUrlOrDir` against the provided engine.
|
|
@@ -29,11 +28,9 @@ import { QuiverRegistry } from "./registry.js";
|
|
|
29
28
|
*/
|
|
30
29
|
export function runQuiverTests(metaUrlOrDir, engine) {
|
|
31
30
|
describe("Quiver", () => {
|
|
32
|
-
let registry;
|
|
33
31
|
let quiver;
|
|
34
32
|
before(async () => {
|
|
35
33
|
quiver = await Quiver.fromDir(metaUrlOrDir);
|
|
36
|
-
registry = new QuiverRegistry({ engine, quivers: [quiver] });
|
|
37
34
|
});
|
|
38
35
|
it("has at least one quill", () => {
|
|
39
36
|
if (quiver.quillNames().length === 0) {
|
|
@@ -43,7 +40,7 @@ export function runQuiverTests(metaUrlOrDir, engine) {
|
|
|
43
40
|
it("compiles every quill version without error", async () => {
|
|
44
41
|
for (const name of quiver.quillNames()) {
|
|
45
42
|
for (const version of quiver.versionsOf(name)) {
|
|
46
|
-
const quill = await
|
|
43
|
+
const quill = await quiver.getQuill(`${name}@${version}`, { engine });
|
|
47
44
|
if (typeof quill.render !== "function") {
|
|
48
45
|
throw new Error(`${name}@${version}: engine returned non-conforming Quill`);
|
|
49
46
|
}
|
package/package.json
CHANGED
package/dist/registry.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { QuillmarkLike, QuillLike } from "./engine-types.js";
|
|
2
|
-
import type { Quiver } from "./quiver.js";
|
|
3
|
-
export declare class QuiverRegistry {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(args: {
|
|
6
|
-
engine: QuillmarkLike;
|
|
7
|
-
quivers: Quiver[];
|
|
8
|
-
});
|
|
9
|
-
/**
|
|
10
|
-
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
11
|
-
*
|
|
12
|
-
* Applies multi-quiver precedence (§4): scan quivers in order, first quiver
|
|
13
|
-
* with any matching candidate wins, highest match within that quiver returned.
|
|
14
|
-
*
|
|
15
|
-
* Throws:
|
|
16
|
-
* - `invalid_ref` if ref fails parseQuillRef
|
|
17
|
-
* - `quill_not_found` if no quiver has a matching candidate
|
|
18
|
-
*/
|
|
19
|
-
resolve(ref: string): Promise<string>;
|
|
20
|
-
/**
|
|
21
|
-
* Returns a render-ready QuillLike instance for a canonical ref.
|
|
22
|
-
* Materializes via engine.quill(tree) on first call; caches by canonical ref.
|
|
23
|
-
*
|
|
24
|
-
* Throws:
|
|
25
|
-
* - `invalid_ref` if canonicalRef is not valid canonical x.y.z form
|
|
26
|
-
* - `quill_not_found` if canonical ref doesn't map to a loaded quiver
|
|
27
|
-
* - propagates I/O errors from loadTree unchanged
|
|
28
|
-
* - propagates engine errors from engine.quill() unchanged (not wrapped)
|
|
29
|
-
*/
|
|
30
|
-
getQuill(canonicalRef: string): Promise<QuillLike>;
|
|
31
|
-
/**
|
|
32
|
-
* Warms all quill refs across all composed quivers. Fail-fast.
|
|
33
|
-
*
|
|
34
|
-
* Calls loadTree + engine.quill(tree) for every known quill version in
|
|
35
|
-
* parallel. Already-cached refs resolve instantly (idempotent). Rejects on
|
|
36
|
-
* the first failure (Promise.all fail-fast semantics).
|
|
37
|
-
*/
|
|
38
|
-
warm(): Promise<void>;
|
|
39
|
-
}
|
package/dist/registry.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { QuiverError } from "./errors.js";
|
|
2
|
-
import { parseQuillRef } from "./ref.js";
|
|
3
|
-
import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
|
|
4
|
-
export class QuiverRegistry {
|
|
5
|
-
#engine;
|
|
6
|
-
#quivers;
|
|
7
|
-
/** Cache: canonical ref → pending or resolved Promise<QuillLike>. */
|
|
8
|
-
#cache = new Map();
|
|
9
|
-
constructor(args) {
|
|
10
|
-
this.#engine = args.engine;
|
|
11
|
-
// Validate no two quivers share Quiver.yaml.name → quiver_collision.
|
|
12
|
-
const seen = new Map();
|
|
13
|
-
for (const quiver of args.quivers) {
|
|
14
|
-
const existing = seen.get(quiver.name);
|
|
15
|
-
if (existing !== undefined) {
|
|
16
|
-
throw new QuiverError("quiver_collision", `Two quivers share the name "${quiver.name}": first quiver and a later quiver both declare this name. ` +
|
|
17
|
-
`Quiver names must be unique within a registry.`, { quiverName: quiver.name });
|
|
18
|
-
}
|
|
19
|
-
seen.set(quiver.name, quiver.name);
|
|
20
|
-
}
|
|
21
|
-
this.#quivers = Object.freeze([...args.quivers]);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
25
|
-
*
|
|
26
|
-
* Applies multi-quiver precedence (§4): scan quivers in order, first quiver
|
|
27
|
-
* with any matching candidate wins, highest match within that quiver returned.
|
|
28
|
-
*
|
|
29
|
-
* Throws:
|
|
30
|
-
* - `invalid_ref` if ref fails parseQuillRef
|
|
31
|
-
* - `quill_not_found` if no quiver has a matching candidate
|
|
32
|
-
*/
|
|
33
|
-
async resolve(ref) {
|
|
34
|
-
// Throws invalid_ref on malformed input.
|
|
35
|
-
const parsed = parseQuillRef(ref);
|
|
36
|
-
for (const quiver of this.#quivers) {
|
|
37
|
-
const versions = quiver.versionsOf(parsed.name);
|
|
38
|
-
if (versions.length === 0)
|
|
39
|
-
continue;
|
|
40
|
-
// Filter by selector if present; otherwise all versions are candidates.
|
|
41
|
-
const candidates = parsed.selector === undefined
|
|
42
|
-
? versions
|
|
43
|
-
: versions.filter((v) => matchesSemverSelector(v, parsed.selector));
|
|
44
|
-
if (candidates.length === 0)
|
|
45
|
-
continue;
|
|
46
|
-
// First quiver with any candidate wins — pick highest within that quiver.
|
|
47
|
-
const winner = chooseHighestVersion(candidates);
|
|
48
|
-
// chooseHighestVersion returns null only for empty arrays; candidates is non-empty.
|
|
49
|
-
return `${parsed.name}@${winner}`;
|
|
50
|
-
}
|
|
51
|
-
throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in any registered quiver.`, { ref });
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Returns a render-ready QuillLike instance for a canonical ref.
|
|
55
|
-
* Materializes via engine.quill(tree) on first call; caches by canonical ref.
|
|
56
|
-
*
|
|
57
|
-
* Throws:
|
|
58
|
-
* - `invalid_ref` if canonicalRef is not valid canonical x.y.z form
|
|
59
|
-
* - `quill_not_found` if canonical ref doesn't map to a loaded quiver
|
|
60
|
-
* - propagates I/O errors from loadTree unchanged
|
|
61
|
-
* - propagates engine errors from engine.quill() unchanged (not wrapped)
|
|
62
|
-
*/
|
|
63
|
-
async getQuill(canonicalRef) {
|
|
64
|
-
let entry = this.#cache.get(canonicalRef);
|
|
65
|
-
if (entry === undefined) {
|
|
66
|
-
entry = this.#loadQuill(canonicalRef).catch((err) => {
|
|
67
|
-
this.#cache.delete(canonicalRef);
|
|
68
|
-
throw err;
|
|
69
|
-
});
|
|
70
|
-
this.#cache.set(canonicalRef, entry);
|
|
71
|
-
}
|
|
72
|
-
return entry;
|
|
73
|
-
}
|
|
74
|
-
/** Internal: does the actual loading work for getQuill. */
|
|
75
|
-
async #loadQuill(canonicalRef) {
|
|
76
|
-
// Parse and validate canonical form (must be x.y.z).
|
|
77
|
-
const parsed = parseQuillRef(canonicalRef);
|
|
78
|
-
if (parsed.selectorDepth !== 3) {
|
|
79
|
-
throw new QuiverError("invalid_ref", `getQuill requires a canonical ref (x.y.z) but received "${canonicalRef}". ` +
|
|
80
|
-
`Use resolve() first to obtain a canonical ref.`, { ref: canonicalRef });
|
|
81
|
-
}
|
|
82
|
-
const version = parsed.selector;
|
|
83
|
-
// Find the first quiver that owns this exact (name, version) pair.
|
|
84
|
-
const owningQuiver = this.#quivers.find((q) => q.versionsOf(parsed.name).includes(version));
|
|
85
|
-
if (owningQuiver === undefined) {
|
|
86
|
-
throw new QuiverError("quill_not_found", `Quill "${canonicalRef}" was not found in any registered quiver.`, { ref: canonicalRef, version });
|
|
87
|
-
}
|
|
88
|
-
// Load the file tree — I/O errors propagate as-is.
|
|
89
|
-
const tree = await owningQuiver.loadTree(parsed.name, version);
|
|
90
|
-
// Materialize the Quill via the engine.
|
|
91
|
-
// Engine errors propagate unchanged — they are not QuiverErrors and we
|
|
92
|
-
// should not mask them. The caller's error-handling stack will see the
|
|
93
|
-
// engine's own error type directly.
|
|
94
|
-
const quill = this.#engine.quill(tree);
|
|
95
|
-
return quill;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Warms all quill refs across all composed quivers. Fail-fast.
|
|
99
|
-
*
|
|
100
|
-
* Calls loadTree + engine.quill(tree) for every known quill version in
|
|
101
|
-
* parallel. Already-cached refs resolve instantly (idempotent). Rejects on
|
|
102
|
-
* the first failure (Promise.all fail-fast semantics).
|
|
103
|
-
*/
|
|
104
|
-
async warm() {
|
|
105
|
-
const promises = [];
|
|
106
|
-
for (const quiver of this.#quivers) {
|
|
107
|
-
for (const name of quiver.quillNames()) {
|
|
108
|
-
for (const version of quiver.versionsOf(name)) {
|
|
109
|
-
promises.push(this.getQuill(`${name}@${version}`));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
await Promise.all(promises);
|
|
114
|
-
}
|
|
115
|
-
}
|