@quillmark/quiver 0.27.0 → 0.28.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/README.md +2 -2
- package/dist/build.js +35 -22
- package/dist/built-loader.d.ts +9 -2
- package/dist/built-loader.js +56 -42
- package/dist/bundle.d.ts +19 -1
- package/dist/bundle.js +8 -8
- package/dist/quiver-yaml.js +2 -4
- package/dist/quiver.js +11 -12
- package/dist/ref.d.ts +0 -2
- package/dist/ref.js +9 -16
- package/dist/semver.d.ts +2 -3
- package/dist/semver.js +5 -17
- package/dist/source-loader.d.ts +2 -2
- package/dist/source-loader.js +9 -4
- package/dist/transports/fs-built-transport.js +2 -2
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +61 -7
- package/dist/transports/memory-transport.d.ts +3 -2
- package/dist/transports/memory-transport.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ const result = await engine.render(quill, doc, { format: 'pdf' });
|
|
|
47
47
|
|
|
48
48
|
That quill is **borrowed, not owned**: every caller asking for that ref gets the same instance, so `free()` on it hands the next caller a freed handle. Code that owns its quill mints one from `(await quiver.getQuill(ref)).toTree()` and frees that.
|
|
49
49
|
|
|
50
|
-
This package produces quills; `@quillmark/wasm` renders them. A quill from `getQuill` is engine-free portable data — schema inspection, validation, blueprint access, `seedDocument()` — and passes straight to `engine.render(quill, doc)`, which routes on `quill.backendId`, loads that backend, clones both handles into its memory, renders, and frees the clones. There is no boundary-crossing step to perform. `Engine.render`, `open
|
|
50
|
+
This package produces quills; `@quillmark/wasm` renders them. A quill from `getQuill` is engine-free portable data — schema inspection, validation, blueprint access, `seedDocument()` — and passes straight to `engine.render(quill, doc)`, which routes on `quill.backendId`, loads that backend, clones both handles into its memory, renders, and frees the clones. There is no boundary-crossing step to perform. `Engine.render`, `open` and `supportedFormats` are **async**. The canonical `Quill` / `Document` / `Engine` types are not re-exported here; import them from the `@quillmark/wasm` peer, their single source of truth.
|
|
51
51
|
|
|
52
52
|
One narrower verb sits beside `getQuill`: `resolve(ref)` returns the canonical ref without materializing anything, and is **sync** (the catalog is in memory from the moment the quiver is built).
|
|
53
53
|
|
|
@@ -124,7 +124,7 @@ The seed answers first and the URL serves what it does not carry. Seeding `lates
|
|
|
124
124
|
|
|
125
125
|
## The `latest.json` pointer
|
|
126
126
|
|
|
127
|
-
`Quiver.fromBuiltUrl(url)` first fetches `<url>/latest.json`, a stable-named pointer to the current manifest. Everything behind that pointer is content-addressed and checked against the digest in its name; the pointer itself is not, so a cache layer can serve a stale one and silently pin the client to the old catalog. It is therefore the one request fetched `no-cache` (revalidate with the origin; a 304 still serves from disk),
|
|
127
|
+
`Quiver.fromBuiltUrl(url)` first fetches `<url>/latest.json`, a stable-named pointer to the current manifest. Everything behind that pointer is content-addressed and checked against the digest in its name; the pointer itself is not, so a cache layer can serve a stale one and silently pin the client to the old catalog. It is therefore the one request fetched `no-cache` (revalidate with the origin; a 304 still serves from disk), and every other request is `force-cache`, a digest-carrying name being entitled to whatever the cache already holds. Both are the browser layer only: a stale CDN edge is answered by that host's cache headers, and `quillkit`'s README states the whole contract for a deploy serving a client over one.
|
|
128
128
|
|
|
129
129
|
The check itself needs `crypto.subtle`, which a browser exposes only in a secure context. An `https` page, `http://localhost` and Node have one; a page served over plain `http` to anything else — a dev host on a LAN address, a staging box without a certificate — has none, and there every fetch passes through unchecked.
|
|
130
130
|
|
package/dist/build.js
CHANGED
|
@@ -76,16 +76,19 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
76
76
|
*/
|
|
77
77
|
export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
78
78
|
// Dynamic imports keep this module safe to type-import from browser contexts.
|
|
79
|
-
const
|
|
80
|
-
const { dirname, join, resolve } =
|
|
79
|
+
const nodePath = await import('node:path');
|
|
80
|
+
const { dirname, join, resolve } = nodePath;
|
|
81
81
|
const { mkdir, rename, rm, writeFile } = await import('node:fs/promises');
|
|
82
82
|
const { existsSync } = await import('node:fs');
|
|
83
83
|
const { createHash } = await import('node:crypto');
|
|
84
84
|
const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
|
|
85
|
-
assertSafeOutDir(
|
|
85
|
+
assertSafeOutDir(nodePath, sourceDir, outDir);
|
|
86
86
|
const out = resolve(outDir);
|
|
87
87
|
const { stage, prev } = stagesOf(out);
|
|
88
88
|
const { meta, catalog } = await scanSourceQuiver(sourceDir);
|
|
89
|
+
// True where the swap stepped the outgoing generation aside and could not put it
|
|
90
|
+
// back: `prev` then holds the only copy, and the sweep below would take it.
|
|
91
|
+
let prevHoldsLastGood = false;
|
|
89
92
|
try {
|
|
90
93
|
await packGeneration();
|
|
91
94
|
}
|
|
@@ -94,7 +97,8 @@ export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
|
94
97
|
// untouched. Neither tree outlives the call either way. Swept rather than
|
|
95
98
|
// checked: a sweep that threw would answer for the build, and what a caller
|
|
96
99
|
// needs to hear is why the build failed.
|
|
97
|
-
|
|
100
|
+
const sweep = prevHoldsLastGood ? [stage] : [stage, prev];
|
|
101
|
+
for (const at of sweep)
|
|
98
102
|
await rm(at, { recursive: true, force: true }).catch(() => { });
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
@@ -113,38 +117,35 @@ export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
|
113
117
|
throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
|
|
114
118
|
}
|
|
115
119
|
const manifestQuills = [];
|
|
120
|
+
// Font hashes already in the store. The store is content-addressed, so a font
|
|
121
|
+
// shared across quills or versions is written once.
|
|
122
|
+
const stored = new Set();
|
|
116
123
|
for (const [quillName, versions] of catalog) {
|
|
117
124
|
for (const version of versions) {
|
|
118
125
|
if (!options.drafts && isDraft(version))
|
|
119
126
|
continue;
|
|
120
127
|
const quillDir = join(sourceDir, 'quills', quillName, version);
|
|
121
128
|
const tree = await readQuillTree(quillDir);
|
|
122
|
-
const fontEntries = [];
|
|
123
|
-
const contentEntries = [];
|
|
124
|
-
for (const [path, bytes] of tree) {
|
|
125
|
-
if (FONT_EXT.test(path)) {
|
|
126
|
-
fontEntries.push([path, bytes]);
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
contentEntries.push([path, bytes]);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
129
|
const fonts = {};
|
|
133
|
-
|
|
130
|
+
const contentRecord = {};
|
|
131
|
+
for (const [rel, bytes] of tree) {
|
|
132
|
+
if (!FONT_EXT.test(rel)) {
|
|
133
|
+
contentRecord[rel] = bytes;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
134
136
|
// Full width: the store is keyed by hash, so two distinct fonts
|
|
135
137
|
// sharing a prefix would merge into one entry.
|
|
136
138
|
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
139
|
+
fonts[rel] = hash;
|
|
140
|
+
if (stored.has(hash))
|
|
141
|
+
continue;
|
|
137
142
|
try {
|
|
138
143
|
await writeFile(join(stage, 'store', hash), bytes);
|
|
139
144
|
}
|
|
140
145
|
catch (err) {
|
|
141
146
|
throw new QuiverError('transport_error', `Failed to write font store entry "${join(outDir, 'store', hash)}": ${err.message}`, { cause: err });
|
|
142
147
|
}
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
const contentRecord = {};
|
|
146
|
-
for (const [path, bytes] of contentEntries) {
|
|
147
|
-
contentRecord[path] = bytes;
|
|
148
|
+
stored.add(hash);
|
|
148
149
|
}
|
|
149
150
|
// The budget `bundle.ts` carries is spent here as well as at the read, so
|
|
150
151
|
// a quill no loader would take is refused by the build that packs it,
|
|
@@ -199,15 +200,27 @@ export async function buildQuiver(sourceDir, outDir, options = {}) {
|
|
|
199
200
|
}
|
|
200
201
|
// `prev` is cleared first: a rename onto a non-empty directory fails, and the
|
|
201
202
|
// tree left by an interrupted run is one.
|
|
203
|
+
//
|
|
204
|
+
// Between them the outgoing generation is only at `prev`, so a second rename that
|
|
205
|
+
// throws — EXDEV across a mount, EBUSY on a tree a reader holds, ENOSPC — puts it
|
|
206
|
+
// back, and where the put-back fails leaves it there for the sweep to spare.
|
|
207
|
+
let steppedAside = false;
|
|
202
208
|
try {
|
|
203
209
|
await mkdir(dirname(out), { recursive: true });
|
|
204
210
|
await rm(prev, { recursive: true, force: true });
|
|
205
|
-
if (existsSync(out))
|
|
211
|
+
if (existsSync(out)) {
|
|
206
212
|
await rename(out, prev);
|
|
213
|
+
steppedAside = true;
|
|
214
|
+
}
|
|
207
215
|
await rename(stage, out);
|
|
208
216
|
}
|
|
209
217
|
catch (err) {
|
|
210
|
-
|
|
218
|
+
if (steppedAside)
|
|
219
|
+
prevHoldsLastGood = !(await rename(prev, out).then(() => true, () => false));
|
|
220
|
+
throw new QuiverError('transport_error', `Failed to move the build into "${outDir}": ${err.message}` +
|
|
221
|
+
(prevHoldsLastGood
|
|
222
|
+
? `. The previous generation is at "${prev}" and has to be moved back by hand`
|
|
223
|
+
: ''), { cause: err });
|
|
211
224
|
}
|
|
212
225
|
}
|
|
213
226
|
}
|
package/dist/built-loader.d.ts
CHANGED
|
@@ -12,14 +12,21 @@ import type { Quiver } from './quiver.js';
|
|
|
12
12
|
* (Node).
|
|
13
13
|
*/
|
|
14
14
|
export interface BuiltTransport {
|
|
15
|
-
fetchBytes(relativePath: string, opts
|
|
15
|
+
fetchBytes(relativePath: string, opts: FetchOptions): Promise<Uint8Array>;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
18
|
* `revalidate` marks the one request that must not be answered from a cache:
|
|
19
19
|
* `latest.json`, the only name in the artifact that is not content-addressed.
|
|
20
|
-
* Everything else is safe to cache forever by construction
|
|
20
|
+
* Everything else is safe to cache forever by construction, and a transport over a
|
|
21
|
+
* cache says so itself rather than leaving the fact to a host's headers.
|
|
22
|
+
*
|
|
23
|
+
* `maxBytes` is what the response may weigh, required because only the caller knows what
|
|
24
|
+
* it asked for: a pointer, a bundle and a font carry their own ceilings. A transport
|
|
25
|
+
* holding a stream refuses past it before the bytes are resident; one over a file or a
|
|
26
|
+
* held map ignores it.
|
|
21
27
|
*/
|
|
22
28
|
export interface FetchOptions {
|
|
29
|
+
maxBytes: number;
|
|
23
30
|
revalidate?: boolean;
|
|
24
31
|
}
|
|
25
32
|
/** Load a build-output quiver via the given transport. */
|
package/dist/built-loader.js
CHANGED
|
@@ -6,25 +6,36 @@
|
|
|
6
6
|
* Package-internal, and browser-safe: no static `node:` imports at any level.
|
|
7
7
|
*/
|
|
8
8
|
import { QuiverError } from './errors.js';
|
|
9
|
-
import { unpackFiles } from './bundle.js';
|
|
9
|
+
import { MAX_BUNDLE_BYTES, unpackFiles } from './bundle.js';
|
|
10
10
|
import { isQuillName } from './ref.js';
|
|
11
11
|
import { isCanonicalSemver, compareSemver } from './semver.js';
|
|
12
12
|
import { NAME_DIGEST_LENGTH, sha256Hex } from './digest.js';
|
|
13
13
|
import { MANIFEST_VERSION, POINTER_FORMAT } from './format.js';
|
|
14
14
|
import { createQuiver } from './quiver.js';
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
*
|
|
16
|
+
* The ceiling per path. A bundle's is the unpack budget itself (`bundle.ts`). The other
|
|
17
|
+
* two are their own: a font is a store entry rather than a bundle entry, and the pointer
|
|
18
|
+
* and the manifest are the small documents a catalog is read out of — the pointer being
|
|
19
|
+
* the one response in the artifact with no digest behind it.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_DOCUMENT_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
const MAX_FONT_BYTES = 32 * 1024 * 1024;
|
|
23
|
+
/**
|
|
24
|
+
* Fetch under `maxBytes`, then check the bytes against the digest their name
|
|
25
|
+
* carries. That check is what makes "content-addressed, safe to cache forever"
|
|
26
|
+
* a property rather than a hope: it catches a corrupted CDN object, a partial
|
|
27
|
+
* sync, and a name reused across releases. A mismatch is a transport failure
|
|
28
|
+
* (the bytes that arrived are not the bytes asked for), so the caches that
|
|
29
|
+
* evict on error let a retry succeed.
|
|
30
|
+
*
|
|
31
|
+
* The ceiling is the transport's to hold, because a digest is a verdict on
|
|
32
|
+
* bytes that have all arrived.
|
|
22
33
|
*
|
|
23
34
|
* Where no digest primitive exists (a page served over plain http, which is
|
|
24
35
|
* not a secure context) the fetch passes through unchecked.
|
|
25
36
|
*/
|
|
26
|
-
async function fetchVerified(transport, path, expected) {
|
|
27
|
-
const bytes = await transport.fetchBytes(path);
|
|
37
|
+
async function fetchVerified(transport, path, expected, maxBytes) {
|
|
38
|
+
const bytes = await transport.fetchBytes(path, { maxBytes });
|
|
28
39
|
const actual = await sha256Hex(bytes);
|
|
29
40
|
if (actual !== undefined && !actual.startsWith(expected)) {
|
|
30
41
|
throw new QuiverError('transport_error', `Digest mismatch for "${path}": the bytes hash to ${actual.slice(0, expected.length)}, not ${expected}`);
|
|
@@ -71,7 +82,7 @@ class BuiltLoader {
|
|
|
71
82
|
// The catalog the outer Quiver resolves against derives from this index,
|
|
72
83
|
// so every ref reaching here has an entry.
|
|
73
84
|
const entry = this.index.get(`${name}@${version}`);
|
|
74
|
-
const zipBytes = await fetchVerified(this.transport, entry.bundle, digestOfName(BUNDLE_FILENAME_RE, entry.bundle, 'bundle filename'));
|
|
85
|
+
const zipBytes = await fetchVerified(this.transport, entry.bundle, digestOfName(BUNDLE_FILENAME_RE, entry.bundle, 'bundle filename'), MAX_BUNDLE_BYTES);
|
|
75
86
|
const files = unpackFiles(zipBytes);
|
|
76
87
|
const fontEntries = Object.entries(entry.fonts);
|
|
77
88
|
await Promise.all(fontEntries.map(async ([path, hash]) => {
|
|
@@ -87,7 +98,7 @@ class BuiltLoader {
|
|
|
87
98
|
fetchFont(hash) {
|
|
88
99
|
let promise = this.fontCache.get(hash);
|
|
89
100
|
if (!promise) {
|
|
90
|
-
promise = fetchVerified(this.transport, `store/${hash}`, hash).catch((err) => {
|
|
101
|
+
promise = fetchVerified(this.transport, `store/${hash}`, hash, MAX_FONT_BYTES).catch((err) => {
|
|
91
102
|
this.fontCache.delete(hash);
|
|
92
103
|
throw err;
|
|
93
104
|
});
|
|
@@ -128,10 +139,11 @@ function parsePointer(raw) {
|
|
|
128
139
|
throw new QuiverError('quiver_invalid', `This quiver is built in format ${format} and this loader reads ${POINTER_FORMAT}. ` +
|
|
129
140
|
`Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
|
|
130
141
|
}
|
|
131
|
-
|
|
142
|
+
const manifest = obj['manifest'];
|
|
143
|
+
if (typeof manifest !== 'string' || manifest.length === 0) {
|
|
132
144
|
throw new QuiverError('quiver_invalid', 'latest.json must have a non-empty string "manifest" field');
|
|
133
145
|
}
|
|
134
|
-
return
|
|
146
|
+
return manifest;
|
|
135
147
|
}
|
|
136
148
|
function parseManifest(raw) {
|
|
137
149
|
let parsed;
|
|
@@ -154,58 +166,57 @@ function parseManifest(raw) {
|
|
|
154
166
|
throw new QuiverError('quiver_invalid', `This quiver's manifest is version ${version} and this loader reads ${MANIFEST_VERSION}. ` +
|
|
155
167
|
`Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
|
|
156
168
|
}
|
|
157
|
-
|
|
169
|
+
const name = obj['name'];
|
|
170
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
158
171
|
throw new QuiverError('quiver_invalid', 'Manifest must have a non-empty string "name" field');
|
|
159
172
|
}
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
const description = obj['description'];
|
|
174
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
175
|
+
throw new QuiverError('quiver_invalid', `Manifest "description" must be a string if present, got ${typeof description}`);
|
|
162
176
|
}
|
|
163
|
-
|
|
177
|
+
const entries = obj['quills'];
|
|
178
|
+
if (!Array.isArray(entries)) {
|
|
164
179
|
throw new QuiverError('quiver_invalid', 'Manifest must have a "quills" array');
|
|
165
180
|
}
|
|
166
181
|
const quills = [];
|
|
167
|
-
for (let i = 0; i <
|
|
168
|
-
const entry =
|
|
182
|
+
for (let i = 0; i < entries.length; i++) {
|
|
183
|
+
const entry = entries[i];
|
|
169
184
|
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
170
185
|
throw new QuiverError('quiver_invalid', `manifest.quills[${i}] must be an object`);
|
|
171
186
|
}
|
|
172
187
|
const e = entry;
|
|
173
188
|
assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'], `manifest.quills[${i}]`);
|
|
174
|
-
|
|
189
|
+
const quillName = e['name'];
|
|
190
|
+
if (typeof quillName !== 'string' || quillName.length === 0) {
|
|
175
191
|
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].name must be a non-empty string`);
|
|
176
192
|
}
|
|
177
|
-
if (!isQuillName(
|
|
178
|
-
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].name "${
|
|
193
|
+
if (!isQuillName(quillName)) {
|
|
194
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].name "${quillName}" is not a name a ref can spell — only [A-Za-z0-9_-] are allowed`);
|
|
179
195
|
}
|
|
180
|
-
|
|
181
|
-
|
|
196
|
+
const version = e['version'];
|
|
197
|
+
if (typeof version !== 'string' || !isCanonicalSemver(version)) {
|
|
198
|
+
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].version must be canonical semver (x.y.z), got "${String(version)}"`);
|
|
182
199
|
}
|
|
183
|
-
|
|
200
|
+
const bundle = e['bundle'];
|
|
201
|
+
if (typeof bundle !== 'string' || bundle.length === 0) {
|
|
184
202
|
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].bundle must be a non-empty string`);
|
|
185
203
|
}
|
|
186
|
-
digestOfName(BUNDLE_FILENAME_RE,
|
|
187
|
-
|
|
204
|
+
digestOfName(BUNDLE_FILENAME_RE, bundle, `manifest.quills[${i}].bundle filename`);
|
|
205
|
+
const declared = e['fonts'];
|
|
206
|
+
if (typeof declared !== 'object' || declared === null || Array.isArray(declared)) {
|
|
188
207
|
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].fonts must be an object`);
|
|
189
208
|
}
|
|
190
|
-
const fonts =
|
|
191
|
-
for (const [k, v] of Object.entries(
|
|
209
|
+
const fonts = {};
|
|
210
|
+
for (const [k, v] of Object.entries(declared)) {
|
|
192
211
|
if (typeof v !== 'string') {
|
|
193
212
|
throw new QuiverError('quiver_invalid', `manifest.quills[${i}].fonts["${k}"] must be a string`);
|
|
194
213
|
}
|
|
195
214
|
validateFontHash(v, `manifest.quills[${i}].fonts["${k}"]`);
|
|
215
|
+
fonts[k] = v;
|
|
196
216
|
}
|
|
197
|
-
quills.push({
|
|
198
|
-
name: e['name'],
|
|
199
|
-
version: e['version'],
|
|
200
|
-
bundle: e['bundle'],
|
|
201
|
-
fonts: fonts
|
|
202
|
-
});
|
|
217
|
+
quills.push({ name: quillName, version, bundle, fonts });
|
|
203
218
|
}
|
|
204
|
-
return {
|
|
205
|
-
name: obj['name'],
|
|
206
|
-
description: obj['description'],
|
|
207
|
-
quills
|
|
208
|
-
};
|
|
219
|
+
return { name, description, quills };
|
|
209
220
|
}
|
|
210
221
|
// ─── Catalog assembly ─────────────────────────────────────────────────────────
|
|
211
222
|
/**
|
|
@@ -232,7 +243,10 @@ export async function loadBuiltQuiver(transport) {
|
|
|
232
243
|
// one fetch that must revalidate.
|
|
233
244
|
let pointerBytes;
|
|
234
245
|
try {
|
|
235
|
-
pointerBytes = await transport.fetchBytes('latest.json', {
|
|
246
|
+
pointerBytes = await transport.fetchBytes('latest.json', {
|
|
247
|
+
revalidate: true,
|
|
248
|
+
maxBytes: MAX_DOCUMENT_BYTES
|
|
249
|
+
});
|
|
236
250
|
}
|
|
237
251
|
catch (err) {
|
|
238
252
|
if (err instanceof QuiverError)
|
|
@@ -243,7 +257,7 @@ export async function loadBuiltQuiver(transport) {
|
|
|
243
257
|
const manifestDigest = digestOfName(MANIFEST_FILENAME_RE, manifestFileName, 'Pointer manifest filename');
|
|
244
258
|
let manifestBytes;
|
|
245
259
|
try {
|
|
246
|
-
manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest);
|
|
260
|
+
manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest, MAX_DOCUMENT_BYTES);
|
|
247
261
|
}
|
|
248
262
|
catch (err) {
|
|
249
263
|
if (err instanceof QuiverError)
|
package/dist/bundle.d.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zip utilities — browser-safe (uses fflate only).
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* What a bundle may hold. Deflate tops out near 1032:1, so a megabyte of zip is a
|
|
6
|
+
* gigabyte resident and the bundle is what a reader inflates to find that out.
|
|
7
|
+
*
|
|
8
|
+
* The ceiling is a constant rather than something a consumer raises, because a quiver
|
|
9
|
+
* is a dependency pinned like any other (README, "What a quiver is trusted to be") and
|
|
10
|
+
* what a budget bounds there is a build that packed something enormous. Both ends spend
|
|
11
|
+
* it, so an artifact no loader would read is refused where it is written.
|
|
12
|
+
*
|
|
13
|
+
* Fonts are not in here: `build` writes them to the store and the manifest names them,
|
|
14
|
+
* leaving a bundle its markdown, its Typst, and what those inline.
|
|
15
|
+
*
|
|
16
|
+
* `built-loader.ts` spends it on the wire too. A deflate of what fits inside it is within
|
|
17
|
+
* zip framing of that, so one number bounds both ends and the fetch needs no second.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_BUNDLE_BYTES: number;
|
|
4
20
|
/**
|
|
5
21
|
* Pack a flat file map into a deterministic zip.
|
|
6
22
|
* Keys are sorted before zipping so insertion order doesn't affect output.
|
|
@@ -10,6 +26,8 @@ export declare function packFiles(files: Record<string, Uint8Array>): Uint8Array
|
|
|
10
26
|
/**
|
|
11
27
|
* Unpack a zip into a flat file map.
|
|
12
28
|
* Returns { path: Uint8Array } for every file entry in the archive.
|
|
13
|
-
* Throws `quiver_invalid` where the budget is spent
|
|
29
|
+
* Throws `quiver_invalid` where the budget is spent. fflate filters and inflates one
|
|
30
|
+
* entry per pass, so the entries the budget already took are resident when it throws —
|
|
31
|
+
* bounded, being what fit inside it.
|
|
14
32
|
*/
|
|
15
33
|
export declare function unpackFiles(data: Uint8Array): Record<string, Uint8Array>;
|
package/dist/bundle.js
CHANGED
|
@@ -25,8 +25,11 @@ const ZIP_EPOCH = new Date(1980, 0, 1, 0, 0, 0, 0);
|
|
|
25
25
|
*
|
|
26
26
|
* Fonts are not in here: `build` writes them to the store and the manifest names them,
|
|
27
27
|
* leaving a bundle its markdown, its Typst, and what those inline.
|
|
28
|
+
*
|
|
29
|
+
* `built-loader.ts` spends it on the wire too. A deflate of what fits inside it is within
|
|
30
|
+
* zip framing of that, so one number bounds both ends and the fetch needs no second.
|
|
28
31
|
*/
|
|
29
|
-
const MAX_BUNDLE_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
export const MAX_BUNDLE_BYTES = 64 * 1024 * 1024;
|
|
30
33
|
const MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
31
34
|
const MAX_BUNDLE_ENTRIES = 2048;
|
|
32
35
|
/**
|
|
@@ -73,19 +76,16 @@ export function packFiles(files) {
|
|
|
73
76
|
/**
|
|
74
77
|
* Unpack a zip into a flat file map.
|
|
75
78
|
* Returns { path: Uint8Array } for every file entry in the archive.
|
|
76
|
-
* Throws `quiver_invalid` where the budget is spent
|
|
79
|
+
* Throws `quiver_invalid` where the budget is spent. fflate filters and inflates one
|
|
80
|
+
* entry per pass, so the entries the budget already took are resident when it throws —
|
|
81
|
+
* bounded, being what fit inside it.
|
|
77
82
|
*/
|
|
78
83
|
export function unpackFiles(data) {
|
|
79
84
|
const spend = bundleBudget();
|
|
80
|
-
|
|
85
|
+
return unzipSync(data, {
|
|
81
86
|
filter: ({ name, originalSize }) => {
|
|
82
87
|
spend(name, originalSize);
|
|
83
88
|
return true;
|
|
84
89
|
}
|
|
85
90
|
});
|
|
86
|
-
const result = {};
|
|
87
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
88
|
-
result[key] = value;
|
|
89
|
-
}
|
|
90
|
-
return result;
|
|
91
91
|
}
|
package/dist/quiver-yaml.js
CHANGED
|
@@ -45,13 +45,11 @@ export function parseQuiverYaml(raw) {
|
|
|
45
45
|
if (!isQuillName(name)) {
|
|
46
46
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
const meta = { name };
|
|
49
|
+
if (doc['description'] !== undefined) {
|
|
49
50
|
if (typeof doc['description'] !== 'string') {
|
|
50
51
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: "description" must be a string if present, got ${typeof doc['description']}`);
|
|
51
52
|
}
|
|
52
|
-
}
|
|
53
|
-
const meta = { name };
|
|
54
|
-
if (typeof doc['description'] === 'string') {
|
|
55
53
|
meta.description = doc['description'];
|
|
56
54
|
}
|
|
57
55
|
return meta;
|
package/dist/quiver.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { QuiverError } from './errors.js';
|
|
11
11
|
import { init } from '@quillmark/wasm';
|
|
12
12
|
import { parseQuillRef } from './ref.js';
|
|
13
|
-
import { matchesSemverSelector
|
|
13
|
+
import { matchesSemverSelector } from './semver.js';
|
|
14
14
|
/**
|
|
15
15
|
* The private constructor's only door, closed over by the static block below.
|
|
16
16
|
* Exported as `createQuiver` for this package's loaders and reachable from no
|
|
@@ -121,17 +121,16 @@ export class Quiver {
|
|
|
121
121
|
* - `quill_not_found` if no version matches
|
|
122
122
|
*/
|
|
123
123
|
resolve(ref) {
|
|
124
|
-
const
|
|
125
|
-
const versions = this.#catalog.get(
|
|
126
|
-
if (versions
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return `${
|
|
134
|
-
}
|
|
124
|
+
const { name, selector } = parseQuillRef(ref);
|
|
125
|
+
const versions = this.#catalog.get(name);
|
|
126
|
+
if (versions !== undefined) {
|
|
127
|
+
// Every loader materializes a catalog sorted descending, so the first match is
|
|
128
|
+
// the highest.
|
|
129
|
+
const winner = selector === undefined
|
|
130
|
+
? versions[0]
|
|
131
|
+
: versions.find((v) => matchesSemverSelector(v, selector));
|
|
132
|
+
if (winner !== undefined)
|
|
133
|
+
return `${name}@${winner}`;
|
|
135
134
|
}
|
|
136
135
|
throw new QuiverError('quill_not_found', `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
|
|
137
136
|
}
|
package/dist/ref.d.ts
CHANGED
|
@@ -3,8 +3,6 @@ export interface ParsedQuillRef {
|
|
|
3
3
|
name: string;
|
|
4
4
|
/** Undefined selects the highest version present. */
|
|
5
5
|
selector?: string;
|
|
6
|
-
/** Selector part count: 1 = `x`, 2 = `x.y`, 3 = `x.y.z` (exact). */
|
|
7
|
-
selectorDepth?: 1 | 2 | 3;
|
|
8
6
|
}
|
|
9
7
|
/**
|
|
10
8
|
* Whether `name` is a name a ref can spell. A quiver's own name and every catalog
|
package/dist/ref.js
CHANGED
|
@@ -22,19 +22,11 @@ export function parseQuillRef(ref) {
|
|
|
22
22
|
if (atIndex === 0) {
|
|
23
23
|
throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return { name };
|
|
31
|
-
}
|
|
32
|
-
const name = ref.slice(0, atIndex);
|
|
33
|
-
const selector = ref.slice(atIndex + 1);
|
|
34
|
-
if (!name) {
|
|
35
|
-
throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
|
|
36
|
-
}
|
|
37
|
-
if (!selector) {
|
|
25
|
+
// Split once: the charset the name is held to is one rule, and a ref with a selector
|
|
26
|
+
// is under it exactly as a bare one is.
|
|
27
|
+
const name = atIndex === -1 ? ref : ref.slice(0, atIndex);
|
|
28
|
+
const selector = atIndex === -1 ? undefined : ref.slice(atIndex + 1);
|
|
29
|
+
if (selector === '') {
|
|
38
30
|
throw new QuiverError('invalid_ref', `Invalid ref: missing selector after "@" in "${ref}"`, {
|
|
39
31
|
ref
|
|
40
32
|
});
|
|
@@ -42,10 +34,11 @@ export function parseQuillRef(ref) {
|
|
|
42
34
|
if (!NAME_RE.test(name)) {
|
|
43
35
|
throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
|
|
44
36
|
}
|
|
37
|
+
if (selector === undefined) {
|
|
38
|
+
return { name };
|
|
39
|
+
}
|
|
45
40
|
if (!SELECTOR_RE.test(selector)) {
|
|
46
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 });
|
|
47
42
|
}
|
|
48
|
-
|
|
49
|
-
const depth = parts.length;
|
|
50
|
-
return { name, selector, selectorDepth: depth };
|
|
43
|
+
return { name, selector };
|
|
51
44
|
}
|
package/dist/semver.d.ts
CHANGED
|
@@ -8,9 +8,8 @@ export declare function isCanonicalSemver(version: string): boolean;
|
|
|
8
8
|
* prefix comparison over an already-valid selector.
|
|
9
9
|
*/
|
|
10
10
|
export declare function matchesSemverSelector(version: string, selector: string): boolean;
|
|
11
|
-
/** Compares two canonical semver strings. Returns <0, 0, or >0.
|
|
11
|
+
/** Compares two canonical semver strings. Returns <0, 0, or >0. Three positions,
|
|
12
|
+
* because `isCanonicalSemver` gates every entry point that reaches here. */
|
|
12
13
|
export declare function compareSemver(a: string, b: string): number;
|
|
13
14
|
/** Returns true for a canonical version below `MIN_PUBLISHED_VERSION`. */
|
|
14
15
|
export declare function isDraft(version: string): boolean;
|
|
15
|
-
/** Returns the highest version string, or null if empty. */
|
|
16
|
-
export declare function chooseHighestVersion(versions: string[]): string | null;
|
package/dist/semver.js
CHANGED
|
@@ -10,8 +10,6 @@ export function isCanonicalSemver(version) {
|
|
|
10
10
|
* prefix comparison over an already-valid selector.
|
|
11
11
|
*/
|
|
12
12
|
export function matchesSemverSelector(version, selector) {
|
|
13
|
-
if (selector === version)
|
|
14
|
-
return true;
|
|
15
13
|
const selectorParts = selector.split('.');
|
|
16
14
|
const versionParts = version.split('.');
|
|
17
15
|
if (selectorParts.length > versionParts.length)
|
|
@@ -22,16 +20,14 @@ export function matchesSemverSelector(version, selector) {
|
|
|
22
20
|
}
|
|
23
21
|
return true;
|
|
24
22
|
}
|
|
25
|
-
/** Compares two canonical semver strings. Returns <0, 0, or >0.
|
|
23
|
+
/** Compares two canonical semver strings. Returns <0, 0, or >0. Three positions,
|
|
24
|
+
* because `isCanonicalSemver` gates every entry point that reaches here. */
|
|
26
25
|
export function compareSemver(a, b) {
|
|
27
26
|
const partsA = a.split('.').map(Number);
|
|
28
27
|
const partsB = b.split('.').map(Number);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const numB = partsB[i] ?? 0;
|
|
33
|
-
if (numA !== numB)
|
|
34
|
-
return numA - numB;
|
|
28
|
+
for (let i = 0; i < 3; i++) {
|
|
29
|
+
if (partsA[i] !== partsB[i])
|
|
30
|
+
return partsA[i] - partsB[i];
|
|
35
31
|
}
|
|
36
32
|
return 0;
|
|
37
33
|
}
|
|
@@ -39,11 +35,3 @@ export function compareSemver(a, b) {
|
|
|
39
35
|
export function isDraft(version) {
|
|
40
36
|
return compareSemver(version, MIN_PUBLISHED_VERSION) < 0;
|
|
41
37
|
}
|
|
42
|
-
/** Returns the highest version string, or null if empty. */
|
|
43
|
-
export function chooseHighestVersion(versions) {
|
|
44
|
-
if (versions.length === 0)
|
|
45
|
-
return null;
|
|
46
|
-
const copy = [...versions];
|
|
47
|
-
copy.sort((a, b) => compareSemver(b, a));
|
|
48
|
-
return copy[0] ?? null;
|
|
49
|
-
}
|
package/dist/source-loader.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ import type { QuiverLoader } from './quiver.js';
|
|
|
11
11
|
* to build a catalog of quill names → sorted versions (descending).
|
|
12
12
|
*
|
|
13
13
|
* Throws:
|
|
14
|
-
* - `quiver_invalid` if Quiver.yaml is
|
|
15
|
-
*
|
|
14
|
+
* - `quiver_invalid` if Quiver.yaml is invalid, any rung it opens or reads is a
|
|
15
|
+
* symlink, a quill dir holding versions is named outside the ref charset, a
|
|
16
16
|
* version dir name is non-canonical, or a version dir is missing its Quill.yaml
|
|
17
17
|
* sentinel.
|
|
18
18
|
* - `transport_error` for I/O failures (permissions, etc.).
|
package/dist/source-loader.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Internal filesystem scanner for the Source Quiver layout. Statically imports
|
|
3
3
|
* `node:fs`, so it is reachable from the Node entry alone.
|
|
4
4
|
*/
|
|
5
|
-
import { readdir, readFile, lstat
|
|
5
|
+
import { readdir, readFile, lstat } from 'node:fs/promises';
|
|
6
6
|
import { join, relative, sep } from 'node:path';
|
|
7
7
|
import { QuiverError } from './errors.js';
|
|
8
8
|
import { parseQuiverYaml } from './quiver-yaml.js';
|
|
@@ -27,8 +27,8 @@ function refuseSymlink(st, path) {
|
|
|
27
27
|
* to build a catalog of quill names → sorted versions (descending).
|
|
28
28
|
*
|
|
29
29
|
* Throws:
|
|
30
|
-
* - `quiver_invalid` if Quiver.yaml is
|
|
31
|
-
*
|
|
30
|
+
* - `quiver_invalid` if Quiver.yaml is invalid, any rung it opens or reads is a
|
|
31
|
+
* symlink, a quill dir holding versions is named outside the ref charset, a
|
|
32
32
|
* version dir name is non-canonical, or a version dir is missing its Quill.yaml
|
|
33
33
|
* sentinel.
|
|
34
34
|
* - `transport_error` for I/O failures (permissions, etc.).
|
|
@@ -39,9 +39,12 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
39
39
|
const quiverYamlPath = join(rootDir, 'Quiver.yaml');
|
|
40
40
|
let raw;
|
|
41
41
|
try {
|
|
42
|
+
refuseSymlink(await lstat(quiverYamlPath), quiverYamlPath);
|
|
42
43
|
raw = await readFile(quiverYamlPath);
|
|
43
44
|
}
|
|
44
45
|
catch (err) {
|
|
46
|
+
if (err instanceof QuiverError)
|
|
47
|
+
throw err;
|
|
45
48
|
// ENOENT → transport_error: a missing Quiver.yaml means the path itself
|
|
46
49
|
// does not point to a quiver — this is a missing-path condition, not a
|
|
47
50
|
// structural violation of a quiver that exists.
|
|
@@ -104,9 +107,11 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
104
107
|
}
|
|
105
108
|
const quillYamlPath = join(versionPath, 'Quill.yaml');
|
|
106
109
|
try {
|
|
107
|
-
await
|
|
110
|
+
refuseSymlink(await lstat(quillYamlPath), quillYamlPath);
|
|
108
111
|
}
|
|
109
112
|
catch (err) {
|
|
113
|
+
if (err instanceof QuiverError)
|
|
114
|
+
throw err;
|
|
110
115
|
const code = err.code;
|
|
111
116
|
if (code === 'ENOENT') {
|
|
112
117
|
throw new QuiverError('quiver_invalid', `Quill "${quillName}@${versionDir}" is missing required "Quill.yaml"`, { quiverName: meta.name, version: versionDir });
|
|
@@ -14,8 +14,8 @@ export class FsBuiltTransport {
|
|
|
14
14
|
constructor(rootDir) {
|
|
15
15
|
this.rootDir = rootDir;
|
|
16
16
|
}
|
|
17
|
-
// No cache sits between this and the bytes,
|
|
18
|
-
// act on here.
|
|
17
|
+
// No cache sits between this and the bytes, and a file is read at the size it
|
|
18
|
+
// is, so `FetchOptions` has nothing to act on here.
|
|
19
19
|
async fetchBytes(relativePath) {
|
|
20
20
|
if (isAbsolute(relativePath)) {
|
|
21
21
|
throw new QuiverError('transport_error', `FsBuiltTransport: absolute paths are not allowed: "${relativePath}"`);
|
|
@@ -8,5 +8,5 @@ import type { BuiltTransport, FetchOptions } from '../built-loader.js';
|
|
|
8
8
|
export declare class HttpTransport implements BuiltTransport {
|
|
9
9
|
private readonly baseUrl;
|
|
10
10
|
constructor(baseUrl: string);
|
|
11
|
-
fetchBytes(relativePath: string, opts
|
|
11
|
+
fetchBytes(relativePath: string, opts: FetchOptions): Promise<Uint8Array>;
|
|
12
12
|
}
|
|
@@ -5,6 +5,36 @@
|
|
|
5
5
|
* Uses globalThis.fetch — no node: imports at any level.
|
|
6
6
|
*/
|
|
7
7
|
import { QuiverError } from '../errors.js';
|
|
8
|
+
/** Over the ceiling is `quiver_invalid`, not `transport_error`: a retry fetches the same
|
|
9
|
+
* response, so there is nothing for an evicting cache to fix. */
|
|
10
|
+
function oversized(url, max, says) {
|
|
11
|
+
return new QuiverError('quiver_invalid', `"${url}" ${says} the ${max} bytes a fetch may read`);
|
|
12
|
+
}
|
|
13
|
+
/** The body chunk by chunk, refused past the ceiling. Cancelling the reader terminates
|
|
14
|
+
* the fetch, so nothing more of a refused response is pulled off the socket. */
|
|
15
|
+
async function readCapped(body, url, max) {
|
|
16
|
+
const reader = body.getReader();
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done)
|
|
22
|
+
break;
|
|
23
|
+
total += value.byteLength;
|
|
24
|
+
if (total > max) {
|
|
25
|
+
await reader.cancel();
|
|
26
|
+
throw oversized(url, max, 'is over');
|
|
27
|
+
}
|
|
28
|
+
chunks.push(value);
|
|
29
|
+
}
|
|
30
|
+
const bytes = new Uint8Array(total);
|
|
31
|
+
let at = 0;
|
|
32
|
+
for (const chunk of chunks) {
|
|
33
|
+
bytes.set(chunk, at);
|
|
34
|
+
at += chunk.byteLength;
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
8
38
|
export class HttpTransport {
|
|
9
39
|
baseUrl;
|
|
10
40
|
constructor(baseUrl) {
|
|
@@ -15,11 +45,13 @@ export class HttpTransport {
|
|
|
15
45
|
const url = `${this.baseUrl}${cleanPath}`;
|
|
16
46
|
let response;
|
|
17
47
|
try {
|
|
18
|
-
// `no-cache` revalidates with the origin rather than skipping the cache:
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
response = await globalThis.fetch(url,
|
|
48
|
+
// `no-cache` revalidates with the origin rather than skipping the cache: a 304
|
|
49
|
+
// still serves from disk. `force-cache` takes a cached response whatever its
|
|
50
|
+
// age, which a digest-carrying name is entitled to; a stale one fails
|
|
51
|
+
// verification upstream as `transport_error`, which an evicting cache clears.
|
|
52
|
+
response = await globalThis.fetch(url, {
|
|
53
|
+
cache: opts.revalidate ? 'no-cache' : 'force-cache'
|
|
54
|
+
});
|
|
23
55
|
}
|
|
24
56
|
catch (err) {
|
|
25
57
|
throw new QuiverError('transport_error', `Network error fetching "${url}": ${err.message}`, { cause: err });
|
|
@@ -27,7 +59,29 @@ export class HttpTransport {
|
|
|
27
59
|
if (!response.ok) {
|
|
28
60
|
throw new QuiverError('transport_error', `HTTP ${response.status} fetching "${url}"`);
|
|
29
61
|
}
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
// A fast reject; the running total below is what holds the ceiling. `Content-Length`
|
|
63
|
+
// is the encoded length, so under `Content-Encoding` it under-reports; a missing one is
|
|
64
|
+
// 0 and an unparseable one NaN, neither over the ceiling, so both accept early.
|
|
65
|
+
const declared = Number(response.headers.get('content-length'));
|
|
66
|
+
if (declared > opts.maxBytes) {
|
|
67
|
+
throw oversized(url, opts.maxBytes, `states ${declared} bytes, over`);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
// A host that hands back no stream is measured after the fact rather than
|
|
71
|
+
// refused: the degraded check still bounds what reaches the unpack budget,
|
|
72
|
+
// and refusing outright would cost hosts that work today.
|
|
73
|
+
if (response.body === null) {
|
|
74
|
+
const buffer = await response.arrayBuffer();
|
|
75
|
+
if (buffer.byteLength > opts.maxBytes)
|
|
76
|
+
throw oversized(url, opts.maxBytes, 'is over');
|
|
77
|
+
return new Uint8Array(buffer);
|
|
78
|
+
}
|
|
79
|
+
return await readCapped(response.body, url, opts.maxBytes);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (err instanceof QuiverError)
|
|
83
|
+
throw err;
|
|
84
|
+
throw new QuiverError('transport_error', `Network error reading "${url}": ${err.message}`, { cause: err });
|
|
85
|
+
}
|
|
32
86
|
}
|
|
33
87
|
}
|
|
@@ -10,6 +10,7 @@ export declare class MemoryTransport implements BuiltTransport {
|
|
|
10
10
|
private readonly fallback?;
|
|
11
11
|
private readonly files;
|
|
12
12
|
constructor(files: ReadonlyMap<string, Uint8Array>, fallback?: BuiltTransport | undefined);
|
|
13
|
-
/** The map answers first. `FetchOptions` reaches only the fallback: a held byte has no
|
|
14
|
-
|
|
13
|
+
/** The map answers first. `FetchOptions` reaches only the fallback: a held byte has no
|
|
14
|
+
* cache to revalidate against and no arrival to refuse. */
|
|
15
|
+
fetchBytes(relativePath: string, opts: FetchOptions): Promise<Uint8Array>;
|
|
15
16
|
}
|
|
@@ -21,7 +21,8 @@ export class MemoryTransport {
|
|
|
21
21
|
this.fallback = fallback;
|
|
22
22
|
this.files = normalizeKeys(files);
|
|
23
23
|
}
|
|
24
|
-
/** The map answers first. `FetchOptions` reaches only the fallback: a held byte has no
|
|
24
|
+
/** The map answers first. `FetchOptions` reaches only the fallback: a held byte has no
|
|
25
|
+
* cache to revalidate against and no arrival to refuse. */
|
|
25
26
|
async fetchBytes(relativePath, opts) {
|
|
26
27
|
const bytes = this.files.get(relativePath);
|
|
27
28
|
if (bytes !== undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"NOTICE"
|
|
43
43
|
],
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@quillmark/wasm": ">=0.
|
|
45
|
+
"@quillmark/wasm": ">=0.113.0-0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"fflate": "^0.8.2",
|