@quillmark/wasm 0.102.0 → 0.103.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/CHANGELOG.md CHANGED
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.103.0 - 2026-08-09
4
+
5
+ - docs: `docs/integration/operations.md`, carrying what the other integration
6
+ pages leave unsaid: that **render is not bounded** — no deadline, no
7
+ cancellation, and the parse limits do not carry through — with the
8
+ worker-termination recipe that is the only abort a browser has; that
9
+ `Quillmark`, `Quill` and `Document` are `Send + Sync`, pinned by a test rather
10
+ than asserted; `comemo` eviction as what a long-lived process's memory tracks;
11
+ the no-network, no-ambient-filesystem isolation properties; and that a panic
12
+ is terminal on every surface.
13
+ - docs: `parse::input_too_large` carries four of the five §8 caps, separable
14
+ only by its `max` arg, which `error-handling.md` now says where it names the
15
+ code.
16
+ - test(cli): `quillmark-cli` gets its first tests. The bin carries
17
+ `test = false`, so twelve cases drive the built executable instead — every
18
+ subcommand, `-o` and `--stdout`, PDF and SVG output, and the error paths,
19
+ which must exit 1 rather than panic. The crate had no `[dev-dependencies]`
20
+ and no workflow invoked it (#1068).
21
+ - fix(cli)!: `render --verbose` writes its progress lines to stderr, as the
22
+ warning printer already did. Under `--stdout` they went to stdout ahead of and
23
+ after the artifact, so `quillmark render q --stdout --verbose > out.pdf`
24
+ produced a PDF with `Loading quill from: …` before its header and
25
+ `Rendering completed successfully` past its trailer. A script that parses
26
+ `--verbose` output from stdout reads it from stderr now.
27
+ - test(fuzz): `pdf_fuzz` covers the AcroForm stamp spine's byte-level reads,
28
+ the one hand-rolled parser with no fuzz target. Arbitrary bytes, and a real
29
+ form truncated, single-byte-corrupted, or spliced, all through
30
+ `page_media_boxes` / `PdfUpdate::begin` / `stamp`. The oracle is no panic:
31
+ nothing in the workspace catches unwind, so a panic there kills the CLI and
32
+ the Python extension and poisons the WASM module. No failures found.
33
+ - fix(core): `MAX_FIELD_COUNT`'s rustdoc said "per document"; the check is per
34
+ card-yaml block, counted after `$`-key extraction.
35
+ - refactor(wasm)!: `init()` resolves to the core surface, and it is the only way
36
+ to reach one. `Quill`, `Document`, `importMarkdown`, `exportMarkdown`,
37
+ `rebase`, `mapPos`, `parseDocPath` and `formatDocPath` leave the static
38
+ exports of `@quillmark/wasm`: `const { Quill, Document } = await init()`
39
+ replaces the value import. The precondition was carried entirely by
40
+ `init`'s signature, and a floating promise is an ESLint rule rather than a
41
+ `tsc` diagnostic, so a call site that skipped the await type-checked and then
42
+ passed or failed by load order. It now has no name to call. `Engine`,
43
+ `MAIN_CARD_ADDR`, `isQuillmarkError`, the open-set guards and the
44
+ writer/reader classes are unchanged, needing no instance or gated by their
45
+ arguments; the `Quill` / `Document` **type** exports are unchanged, so
46
+ annotations and `import type` compile as before. Class identity is untouched:
47
+ the gate hands out the core build's classes verbatim, and `instanceof` stays
48
+ the whole membership test. `runtime::not_initialized` and the build-time
49
+ sentinel that raised it retire with the door they guarded. Rust, Python,
50
+ documents and stored blobs are unaffected. See
51
+ `docs/migrations/0.102-to-0.103.md`
52
+
53
+ <!-- seed: commits since v0.102.0, confirm the entries above cover them, then delete this comment
54
+ - ci: tag the merge commit, and serialize a release against itself
55
+ - ci: drop the tagger GitHub App from the release workflows
56
+ - Cut SECURITY.md, and operations.md down to what only it can say
57
+ - Drop the audit gate; keep the reachability finding in ci.yml's note
58
+ - Fix two wrong claims, a dead property, and the stdout bug the review found
59
+ - dense-prose pass over the added prose, and read the fixture once
60
+ - Close four production-readiness gaps: audit gate, ops docs, CLI tests, PDF fuzzing
61
+ - Drop the README's claim about the deleted sentinel patch
62
+ - dense-prose pass: cut the restatement the change introduced
63
+ - docs: the 0.102 → 0.103 step, and correct the surface it describes
64
+ - The gate is the only door to the core surface
65
+ - docs(migration): a span guide for 0.92 → 0.102
66
+ - Cut over-specified style rules; author preference wins
67
+ - dense-prose: remove the em-dash ban
68
+ - dense-prose: drop the line-budget clause
69
+ - dense-prose: wrong is worse than missing; relax em-dash ban
70
+ - docs: dense-prose pass over the conform-scope and date-grammar prose
71
+ - docs: migration guides are era-stamped, not immutable
72
+ - docs(migrations): give the datetime split its corpus audit step
73
+ - docs: scope conform to content fields, state the date corpus precondition
74
+ - Tighten the failure-delivery prose
75
+ - Deliver init's conflict as a rejection, not a synchronous throw
76
+ -->
77
+
78
+
3
79
  ## v0.102.0 - 2026-08-04
4
80
 
5
81
  The pre-1.0 vocabulary reset. Verbs, diagnostic codes, and two words that meant
package/README.md CHANGED
@@ -10,11 +10,11 @@ Use Quillmark in browsers/Node.js with explicit in-memory trees (`Map<string, Ui
10
10
 
11
11
  The package exposes **one import surface**:
12
12
 
13
- - `@quillmark/wasm` (the root), the **canonical API**: `Quill`, `Document`, and
14
- an `Engine` that renders them.
13
+ - `@quillmark/wasm` (the root), the **canonical API**: `init`, resolving to
14
+ `Quill` and `Document`, and an `Engine` that renders them.
15
15
 
16
- `Quill` and `Document` are re-exported verbatim from the internal Typst-less
17
- core build, so editor/validation code (`Quill.fromTree`,
16
+ `Quill` and `Document` are the internal Typst-less core build's own classes,
17
+ handed out verbatim by `init`, so editor/validation code (`Quill.fromTree`,
18
18
  `Document.fromMarkdown`) loads only that small core binary: no backend is
19
19
  loaded until you render. The `Engine` hides everything else: each backend
20
20
  (`typst`, `pdfform`) is a separate, private WASM binary with its own linear
@@ -31,9 +31,8 @@ bash scripts/build-wasm.sh
31
31
  The script builds three variants: the core (no backend), the Typst backend
32
32
  (default features), and the Typst-free pdfform backend (`pdfform` feature):
33
33
  each with `--target web` and `--weak-refs` enabled (see
34
- [Initialization](#initialization) and [Lifecycle](#lifecycle)). It then patches
35
- each generated build with the pre-init sentinel (`runtime/uninit.js`) and
36
- asserts none of them carries a `.wasm` ESM import or a top-level await.
34
+ [Initialization](#initialization) and [Lifecycle](#lifecycle)). It then asserts
35
+ none of them carries a `.wasm` ESM import or a top-level await.
37
36
 
38
37
  ## Test
39
38
 
@@ -47,9 +46,9 @@ npm test
47
46
  ## Usage
48
47
 
49
48
  ```ts
50
- import { init, Document, Quill, Engine } from "@quillmark/wasm";
49
+ import { init, Engine } from "@quillmark/wasm";
51
50
 
52
- await init(); // once at startup; see Initialization
51
+ const { Quill, Document } = await init(); // see Initialization
53
52
 
54
53
  const quill = Quill.fromTree(tree); // no engine needed: build + validate
55
54
  const engine = new Engine(); // loads a backend lazily on first render
@@ -68,12 +67,13 @@ const result = await engine.render(quill, parsed, { format: "pdf" });
68
67
 
69
68
  ## Initialization
70
69
 
71
- `await init()` once, at startup, before any other export is used. Everything
72
- after it is the synchronous surface the rest of this README describes.
70
+ `init` resolves to the core surface: `Quill`, `Document`, and the free
71
+ functions. Everything after the await is the synchronous surface the rest of
72
+ this README describes.
73
73
 
74
74
  ```js
75
- import { init, Quill, Engine } from "@quillmark/wasm";
76
- await init();
75
+ import { init, Engine } from "@quillmark/wasm";
76
+ const { Quill, Document } = await init();
77
77
  ```
78
78
 
79
79
  The same line works everywhere: the binary streams from a URL in a browser and
@@ -83,10 +83,11 @@ the builds are `--target web`, so nothing in the package graph imports a `.wasm`
83
83
  module or carries a top-level await, and a static `import` of this package is
84
84
  safe anywhere, SSR included.
85
85
 
86
- `init` is idempotent and concurrency-safe: every call returns the same promise,
87
- so several entry points may each `await init()` for one instantiation. A failed
88
- init clears the memo, so a retry works. Each realm initializes its own copy; a
89
- Worker calls `init()` too.
86
+ `init` is idempotent and concurrency-safe: every non-conflicting call returns
87
+ the same promise, so several entry points may each `await init()` for one
88
+ instantiation. Destructure at **every** entry point (route loader, hydration
89
+ path, worker) rather than threading one result around. A failed init clears the
90
+ memo, so a retry works. Each realm initializes its own copy, a Worker included.
90
91
 
91
92
  **Backends need nothing.** `Engine` instantiates a backend inside its lazy load,
92
93
  on the first render against it.
@@ -94,13 +95,17 @@ on the first render against it.
94
95
  **Overriding the source.** `init(source)` accepts bytes, a `Response`, a
95
96
  `WebAssembly.Module`, or a URL, for hosts that route assets themselves or embed
96
97
  the binary. Pass it on the first call; a later call passing a *different* source
97
- throws `runtime::init_conflict` rather than silently ignoring it. Passing the
98
- same value again is fine, so several entry points may each `await init(BYTES)`
99
- against one constant.
98
+ rejects with `runtime::init_conflict` rather than silently ignoring it. Passing
99
+ the same value again is fine, so several entry points may each
100
+ `await init(BYTES)` against one constant.
100
101
 
101
- **If you forget.** Reaching the surface early throws a `QuillmarkError` coded
102
- `runtime::not_initialized` that names the fix, rather than a `TypeError` from
103
- inside generated code.
102
+ **Both failures reject.** `runtime::init_conflict` and `runtime::init_failed`
103
+ alike ride the returned promise, so one `catch` around `await init(...)` covers
104
+ the gate. See [Errors](#errors) for the rule this follows.
105
+
106
+ **You cannot forget.** The core surface has no static export, so a call site
107
+ that skips the await has no name to call. The precondition is structural rather
108
+ than a convention: no load order can make one entry point pass and another fail.
104
109
 
105
110
  **Vite's dev server** pre-bundles dependencies, which moves the package away
106
111
  from its binary. Exclude it:
@@ -547,6 +552,14 @@ try {
547
552
  }
548
553
  ```
549
554
 
555
+ **Delivery follows the function, not the failure.** A synchronous method throws;
556
+ a promise-returning one rejects. The promise-returning surface is `init` and the
557
+ four `Engine` verbs (`render`, `open`, `supportedFormats`, `supportsCanvas`), so
558
+ a programming error reached through one of them (a foreign handle, an
559
+ unregistered backend) rejects like any other failure. Nothing here both returns
560
+ a promise and throws, so a `.catch` on a promise-returning call is a whole
561
+ guard.
562
+
550
563
  `QuillmarkError` is a **structural interface, not a class**: the WASM layer
551
564
  throws a real `Error` and attaches the property, so there is no constructor to
552
565
  `instanceof` against; narrow with `isQuillmarkError` (which also works on
@@ -2885,8 +2885,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
2885
2885
 
2886
2886
  let WASM_VECTOR_LEN = 0;
2887
2887
 
2888
- import { uninitSentinel, UNINIT } from "../../runtime/uninit.js";
2889
- let wasmModule, wasm = uninitSentinel("@quillmark/wasm internal error: the 'pdfform' backend was used before instantiation.", "This is a bug in @quillmark/wasm, not in your code. Please report it.");
2888
+ let wasmModule, wasm;
2890
2889
  function __wbg_finalize_init(instance, module) {
2891
2890
  wasm = instance.exports;
2892
2891
  wasmModule = module;
@@ -2933,7 +2932,7 @@ async function __wbg_load(module, imports) {
2933
2932
  }
2934
2933
 
2935
2934
  function initSync(module) {
2936
- if (wasm !== undefined && !wasm[UNINIT]) return wasm;
2935
+ if (wasm !== undefined) return wasm;
2937
2936
 
2938
2937
 
2939
2938
  if (module !== undefined) {
@@ -2953,7 +2952,7 @@ function initSync(module) {
2953
2952
  }
2954
2953
 
2955
2954
  async function __wbg_init(module_or_path) {
2956
- if (wasm !== undefined && !wasm[UNINIT]) return wasm;
2955
+ if (wasm !== undefined) return wasm;
2957
2956
 
2958
2957
 
2959
2958
  if (module_or_path !== undefined) {
Binary file
@@ -2909,8 +2909,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
2909
2909
 
2910
2910
  let WASM_VECTOR_LEN = 0;
2911
2911
 
2912
- import { uninitSentinel, UNINIT } from "../../runtime/uninit.js";
2913
- let wasmModule, wasm = uninitSentinel("@quillmark/wasm internal error: the 'typst' backend was used before instantiation.", "This is a bug in @quillmark/wasm, not in your code. Please report it.");
2912
+ let wasmModule, wasm;
2914
2913
  function __wbg_finalize_init(instance, module) {
2915
2914
  wasm = instance.exports;
2916
2915
  wasmModule = module;
@@ -2957,7 +2956,7 @@ async function __wbg_load(module, imports) {
2957
2956
  }
2958
2957
 
2959
2958
  function initSync(module) {
2960
- if (wasm !== undefined && !wasm[UNINIT]) return wasm;
2959
+ if (wasm !== undefined) return wasm;
2961
2960
 
2962
2961
 
2963
2962
  if (module !== undefined) {
@@ -2977,7 +2976,7 @@ function initSync(module) {
2977
2976
  }
2978
2977
 
2979
2978
  async function __wbg_init(module_or_path) {
2980
- if (wasm !== undefined && !wasm[UNINIT]) return wasm;
2979
+ if (wasm !== undefined) return wasm;
2981
2980
 
2982
2981
 
2983
2982
  if (module_or_path !== undefined) {
Binary file
package/core/wasm.js CHANGED
@@ -2377,8 +2377,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
2377
2377
 
2378
2378
  let WASM_VECTOR_LEN = 0;
2379
2379
 
2380
- import { uninitSentinel, UNINIT } from "../runtime/uninit.js";
2381
- let wasmModule, wasm = uninitSentinel("@quillmark/wasm is not initialized. Call 'await init()' once at startup, before Quill, Document, Engine, or any other export is used.", "Add: import { init } from '@quillmark/wasm'; await init(); once, anywhere before first use. Extra calls are free.");
2380
+ let wasmModule, wasm;
2382
2381
  function __wbg_finalize_init(instance, module) {
2383
2382
  wasm = instance.exports;
2384
2383
  wasmModule = module;
@@ -2424,7 +2423,7 @@ async function __wbg_load(module, imports) {
2424
2423
  }
2425
2424
 
2426
2425
  function initSync(module) {
2427
- if (wasm !== undefined && !wasm[UNINIT]) return wasm;
2426
+ if (wasm !== undefined) return wasm;
2428
2427
 
2429
2428
 
2430
2429
  if (module !== undefined) {
@@ -2444,7 +2443,7 @@ function initSync(module) {
2444
2443
  }
2445
2444
 
2446
2445
  async function __wbg_init(module_or_path) {
2447
- if (wasm !== undefined && !wasm[UNINIT]) return wasm;
2446
+ if (wasm !== undefined) return wasm;
2448
2447
 
2449
2448
 
2450
2449
  if (module_or_path !== undefined) {
package/core/wasm_bg.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.102.0",
3
+ "version": "0.103.0",
4
4
  "description": "WebAssembly bindings for Quillmark, a schema-driven document engine",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,46 +1,82 @@
1
1
  // @quillmark/wasm/runtime: canonical consumer API.
2
2
  //
3
- // `Quill`/`Document` are re-exported verbatim from the core build (their full
4
- // surface, no drift). Render-side types (`RenderResult`, `RenderOptions`,
5
- // `Artifact`, `OutputFormat`, `PageSize`, `PaintOptions`, `PaintResult`) are
6
- // defined HERE as the canonical, backend-neutral render contract: NOT sourced
7
- // from any one private backend build. A type-level drift guard
8
- // (`runtime.types.test-d.ts`, via `npm run typecheck`) asserts they stay
9
- // mutually assignable with the Typst backend's generated declarations. `Engine`
10
- // is the render dispatcher that hides the cross-WASM-memory seam.
11
-
12
- // CANONICAL INVARIANT: the root re-exports the core build's `Quill`/`Document`
13
- // verbatim; they are the SAME classes, never wrappers. There is exactly one
14
- // public entry point, so this is a structural fact. Replacing the re-export
15
- // with a wrapper is a breaking design change, not a refactor. See runtime.js.
3
+ // Render-side types (`RenderResult`, `RenderOptions`, `Artifact`,
4
+ // `OutputFormat`, `PageSize`, `PaintOptions`, `PaintResult`) are defined HERE as
5
+ // the canonical, backend-neutral render contract: NOT sourced from any one
6
+ // private backend build. A type-level drift guard (`runtime.types.test-d.ts`,
7
+ // via `npm run typecheck`) asserts they stay mutually assignable with the Typst
8
+ // backend's generated declarations. `Engine` is the render dispatcher that hides
9
+ // the cross-WASM-memory seam.
10
+
11
+ // CANONICAL INVARIANT: the `Quill`/`Document` `init` resolves to ARE the core
12
+ // build's classes, their full surface, never wrappers. There is exactly one
13
+ // public entry point, so this is a structural fact. Handing out a wrapper is a
14
+ // breaking design change, not a refactor. See runtime.js.
16
15
  //
17
16
  // ONE COPY PER PROCESS: two copies of this package are two WASM linear memories
18
17
  // and two `Quill`/`Document` classes. Every method taking a handle refuses one
19
18
  // belonging to another copy, with a `QuillmarkError` naming `npm ls
20
19
  // @quillmark/wasm`. Errors are the exception: `isQuillmarkError` is structural.
21
- export { Quill, Document } from '../core/wasm.js';
22
20
 
23
- import type { InitInput } from '../core/wasm.js';
21
+ // The instance types, so an annotation (`let q: Quill`) needs no await. Their
22
+ // values are `CoreSurface`'s.
23
+ export type { Quill, Document } from '../core/wasm.js';
24
+
25
+ import type {
26
+ InitInput,
27
+ Quill as CoreQuill,
28
+ Document as CoreDocument,
29
+ importMarkdown,
30
+ exportMarkdown,
31
+ rebase,
32
+ mapPos,
33
+ parseDocPath,
34
+ formatDocPath
35
+ } from '../core/wasm.js';
24
36
 
25
37
  /**
26
- * Instantiate the core WASM build. Call once at startup, before any other
27
- * export is used; extra calls are free.
38
+ * The core build's surface: what its WASM instance stands behind, and therefore
39
+ * what `init` resolves to. Exported nowhere statically, so awaiting is the only
40
+ * way to hold one.
28
41
  *
29
- * ```js
30
- * import { init, Quill, Engine } from '@quillmark/wasm';
31
- * await init();
42
+ * `Quill` and `Document` here are the classes, statics included
43
+ * (`Quill.fromTree`, `Document.fromMarkdown`), not the instance types above.
44
+ * Each member carries the core build's own declaration, docs and all.
45
+ */
46
+ export interface CoreSurface {
47
+ Quill: typeof CoreQuill;
48
+ Document: typeof CoreDocument;
49
+ importMarkdown: typeof importMarkdown;
50
+ exportMarkdown: typeof exportMarkdown;
51
+ rebase: typeof rebase;
52
+ mapPos: typeof mapPos;
53
+ parseDocPath: typeof parseDocPath;
54
+ formatDocPath: typeof formatDocPath;
55
+ }
56
+
57
+ /**
58
+ * Instantiate the core WASM build and resolve to its surface.
59
+ *
60
+ * ```ts
61
+ * import { init } from '@quillmark/wasm';
62
+ * const { Quill, Document } = await init();
32
63
  * ```
33
64
  *
34
65
  * The builds are `--target web`: classes export synchronously, the instance
35
66
  * behind them arrives here. Identical in every environment: the binary is
36
67
  * fetched and streamed in a browser, read off disk under Node, and the call
37
- * site is the same line. Reaching a class before this resolves throws
38
- * `runtime::not_initialized` naming the fix.
68
+ * site is the same line.
69
+ *
70
+ * THE ONLY DOOR to `Quill`, `Document` and the free functions, so the pre-init
71
+ * mistake is not expressible. Destructure at each entry point (route loader,
72
+ * hydration path, worker) rather than threading one result around: the gate is
73
+ * memoized and concurrency-safe, so every await after the first is free. A
74
+ * failed init clears the memo, so a retry is possible. Per realm: a Worker
75
+ * loads and initializes its own copy.
39
76
  *
40
- * Idempotent and concurrency-safe: every call returns the same promise, so
41
- * `await init()` at several entry points costs one instantiation. A failed init
42
- * clears the memo, so a retry is possible. Per realm: a Worker loads and
43
- * initializes its own copy.
77
+ * Both failure codes REJECT, so one `catch` covers the gate. Delivery follows
78
+ * the function kind across this surface: a sync verb throws, a
79
+ * promise-returning verb rejects, and nothing does both.
44
80
  *
45
81
  * Backends are NOT initialized here. `Engine` instantiates a backend inside its
46
82
  * lazy load, on first render against it.
@@ -48,15 +84,10 @@ import type { InitInput } from '../core/wasm.js';
48
84
  * @param source override the binary's source (bytes, a `Response`, a
49
85
  * `WebAssembly.Module`, a URL) for hosts that route assets themselves or
50
86
  * embed the binary. Pass it on the FIRST call; a later call passing a
51
- * different source throws `runtime::init_conflict` rather than silently
87
+ * different source rejects with `runtime::init_conflict` rather than silently
52
88
  * ignoring it. Passing the same value again is fine.
53
89
  */
54
- export declare function init(source?: InitInput): Promise<void>;
55
- // The document-free content codec, re-exported from the core build.
56
- export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
57
- // The document-model path parser/serializer: route on `Diagnostic.path`
58
- // segments instead of regexing the string.
59
- export { parseDocPath, formatDocPath } from '../core/wasm.js';
90
+ export declare function init(source?: InitInput): Promise<CoreSurface>;
60
91
 
61
92
  import type { CardAddr } from '../core/wasm.js';
62
93
 
@@ -2,15 +2,17 @@
2
2
  //
3
3
  // @quillmark/wasm/runtime: the canonical consumer API.
4
4
  //
5
- // Consumers import `Quill`, `Document`, and `Engine` from here and never touch
6
- // the build-specific subpaths. The package ships multiple WASM binaries with
7
- // SEPARATE linear memories: a Typst-less `core` build (small, eager) that is
8
- // the canonical home of `Quill`/`Document`, and one private backend binary per
9
- // backend (`backends/typst/` today; more later) that carries an engine. A
10
- // handle from one memory cannot be used by another. This module hides that seam
11
- // and is exposed at the package root (`@quillmark/wasm`):
5
+ // Consumers reach `Quill` and `Document` through `await init()` and `Engine`
6
+ // as a static export, and never touch the build-specific subpaths. The package
7
+ // ships multiple WASM binaries with SEPARATE linear memories: a Typst-less
8
+ // `core` build (small, eager) that is the canonical home of `Quill`/`Document`,
9
+ // and one private backend binary per backend (`backends/typst/` today; more
10
+ // later) that carries an engine. A handle from one memory cannot be used by
11
+ // another. This module hides that seam and is exposed at the package root
12
+ // (`@quillmark/wasm`):
12
13
  //
13
- // - `Quill` and `Document` ARE the core build's classes, re-exported. They
14
+ // - `Quill` and `Document` ARE the core build's classes, handed out by the
15
+ // gate (§ "Initialization"). They
14
16
  // hold the canonical data and the full sync surface (schema / validate /
15
17
  // seed / mutate / toJson / toTree). No backend is loaded to use them, so
16
18
  // the editor/validation path never pays for a multi-MB backend binary.
@@ -38,51 +40,42 @@
38
40
  // The cross-memory crossing is therefore invisible: a consumer hands canonical
39
41
  // `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
40
42
 
41
- // ── CANONICAL INVARIANT: re-export the core build, never wrap ───────────────
42
- // The root re-exports the core build's `Quill`/`Document` classes verbatim,
43
- // NOT subclasses or wrappers. There is exactly ONE public entry point (this
44
- // module), so this identity is a structural fact: `Quill`/`Document` ARE the
45
- // core classes, and the only boundary that needs crossing is core→backend (a
46
- // separate WASM memory), which `Engine` does internally as data
47
- // (`toTree`/`toJson`).
43
+ // ── CANONICAL INVARIANT: hand out the core build's classes, never wrap ──────
44
+ // The `Quill`/`Document` a consumer holds ARE the core build's classes, NOT
45
+ // subclasses or wrappers. `init` resolves to them "Initialization"); which
46
+ // door they come through changes nothing about the identity. The only boundary
47
+ // that needs crossing is core→backend (a separate WASM memory), which `Engine`
48
+ // does internally as data (`toTree`/`toJson`).
48
49
  //
49
- // Do NOT replace this with a wrapper class: that breaks the identity and turns
50
- // a structural fact into a converted type (a breaking design change, not a
51
- // refactor). The `runtime.test.js` "re-exports the internal core build classes
52
- // verbatim" case (`Quill === CoreQuill`) is the executable guard for this
53
- // invariant.
54
- //
55
- // It is also why the pre-init guard sits inside the generated builds instead
56
- // of here: nothing may stand between a consumer and these classes
57
- // (runtime/uninit.js).
50
+ // Do NOT hand out a wrapper: that breaks the identity and turns a structural
51
+ // fact into a converted type (a breaking design change, not a refactor). The
52
+ // `runtime.test.js` "hands out the internal core build classes verbatim" case
53
+ // (`Quill === CoreQuill`) is the executable guard for this invariant.
58
54
  //
59
55
  // The identity is what makes `instanceof` the whole membership test: a handle
60
56
  // either belongs to this copy's classes or it belongs to another copy, and the
61
57
  // second is always a consumer bug. `Engine` is NOT duck-typed on its inputs; it
62
58
  // checks them. See § "Handles from another copy" below.
63
59
  //
64
- // Imported (not bare re-exported) so `Quill` is a local binding this module can
65
- // augment: `quill.writer(doc)` is patched onto its prototype below. The
66
- // re-export keeps the identity: the exported `Quill` IS the core class.
60
+ // Local bindings, so this module can augment them: `quill.writer(doc)` is
61
+ // patched onto the prototype below, and `instanceof` reads them directly.
67
62
  //
68
63
  // The default import is the core build's generated instantiation entry
69
64
  // (`--target web`); `init` below is the only thing that calls it.
70
65
  import initCore, { Quill, Document } from '../core/wasm.js';
71
- export { Quill, Document };
72
66
  // The wasm byte source, resolved per environment by package.json's `imports`
73
67
  // map: a pass-through in a browser (the glue fetches and streams the URL
74
68
  // itself), a `node:fs` read under Node, whose `fetch` rejects `file:` URLs.
75
69
  // Resolution-time, so `node:fs` never enters a browser graph.
76
70
  import { toModuleSource } from '#quillmark-env';
77
- // The document-free content codec: re-exported verbatim from the core build so
78
- // the runtime subpath exposes `exportMarkdown(body)` (the on-demand markdown
79
- // projection), `importMarkdown`, and the position-mapping pair (`rebase`,
80
- // `mapPos`).
81
- export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
71
+ // The document-free content codec: `exportMarkdown(body)` (the on-demand
72
+ // markdown projection), `importMarkdown`, and the position-mapping pair
73
+ // (`rebase`, `mapPos`).
74
+ import { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
82
75
  // The document-model path parser/serializer: `parseDocPath(str) => DocPathSeg[]`
83
76
  // and its inverse `formatDocPath`, so a consumer routes on `Diagnostic.path`
84
77
  // segments instead of reverse-engineering the grammar.
85
- export { parseDocPath, formatDocPath } from '../core/wasm.js';
78
+ import { parseDocPath, formatDocPath } from '../core/wasm.js';
86
79
 
87
80
  // ── Initialization ──────────────────────────────────────────────────────────
88
81
  // The builds are `--target web`: they export their classes synchronously but
@@ -90,64 +83,129 @@ export { parseDocPath, formatDocPath } from '../core/wasm.js';
90
83
  // that for core, behind one awaited gate; `Engine` owns it for the backends,
91
84
  // inside their lazy load, so a consumer never initializes a backend by hand.
92
85
  //
86
+ // THE GATE IS THE ONLY DOOR. `init` resolves to the core surface, and this
87
+ // module exports none of it statically, so a handle is unobtainable without
88
+ // having awaited: the precondition is structural rather than a convention the
89
+ // caller has to know. package.json's `exports` map carries exactly one entry,
90
+ // so there is no subpath around the gate either.
91
+ //
93
92
  // The gate is the shape the lazy-backend idiom (§ DEFAULT_BACKENDS) takes when
94
93
  // the surface it guards cannot be async: `Quill.fromTree` and `seedDocument`
95
94
  // are sync and static, so there is nowhere to hide an await except in front.
96
95
  //
97
- // Reaching core before the gate resolves is not a silent wrong answer: the
98
- // build is patched to throw `runtime::not_initialized` naming the fix
99
- // (runtime/uninit.js).
96
+ // WHAT STAYS STATIC is what needs no instance. `MAIN_CARD_ADDR`, the open-set
97
+ // guards and `isQuillmarkError` are pure JS over plain objects; gating them
98
+ // would cost a consumer of one an await it has no use for.
99
+ //
100
+ // The classes stay static too, and are gated by their ARGUMENTS. Every `Engine`
101
+ // verb takes a `Quill` first (`#backendOf` is the single reader), and the
102
+ // writer/reader constructors take both handles, so a caller who has not awaited
103
+ // cannot produce an argument to call them with. `new Engine()` alone touches no
104
+ // wasm: it validates a descriptor map. Holding them out of the gate keeps them
105
+ // tree-shakable, so the editor path drops the dispatcher it never calls.
106
+ //
107
+ // FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
108
+ // throws, a promise-returning verb rejects, and nothing does both. A
109
+ // programming error reached through a promise-returning verb
110
+ // (`runtime::foreign_handle` inside `Engine.render`) rejects like any other.
111
+ // `init` is the one promise-returning export not declared `async`, because the
112
+ // memo is returned by identity; its conflict guard rejects explicitly to hold
113
+ // the rule, which the return type cannot declare and a `.catch` would not see.
100
114
 
101
- /** The in-flight or settled core instantiation. The memo is the PROMISE, not a
102
- * boolean, so concurrent callers share one instantiation instead of racing. */
115
+ /**
116
+ * The gated surface: the core build's values, which are exactly the ones its
117
+ * instance stands behind. Frozen and built at module scope, because the classes
118
+ * and functions themselves resolve synchronously (only the instance behind them
119
+ * is late), so there is nothing to defer and no per-call allocation.
120
+ *
121
+ * Membership is derived, not chosen: it is the core build's exports minus its
122
+ * instantiation machinery (`default`, `initSync`, and the start-section
123
+ * `start`), which `init` owns and no consumer calls. `init.test.js` § "the
124
+ * gated surface" computes that set and pins it, so a new core export that never
125
+ * reaches here fails there rather than going missing.
126
+ * @type {import('./runtime.js').CoreSurface}
127
+ */
128
+ const CORE_SURFACE = Object.freeze({
129
+ Quill,
130
+ Document,
131
+ importMarkdown,
132
+ exportMarkdown,
133
+ rebase,
134
+ mapPos,
135
+ parseDocPath,
136
+ formatDocPath
137
+ });
138
+
139
+ /** The in-flight or settled core instantiation, resolving to `CORE_SURFACE`.
140
+ * The memo is the PROMISE, not a boolean, so concurrent callers share one
141
+ * instantiation instead of racing.
142
+ * @type {Promise<import('./runtime.js').CoreSurface> | undefined} */
103
143
  let coreInit;
104
144
  /** The source `init` was first called with; the conflict check reads it. */
105
145
  let coreInitSource;
106
146
 
107
147
  /**
108
- * Instantiate the core WASM build. Call once at startup, before any other
109
- * export is used; extra calls are free.
148
+ * Instantiate the core WASM build and resolve to its surface.
110
149
  *
111
150
  * ```js
112
- * import { init, Quill, Engine } from '@quillmark/wasm';
113
- * await init();
151
+ * import { init } from '@quillmark/wasm';
152
+ * const { Quill, Document } = await init();
114
153
  * ```
115
154
  *
155
+ * The classes and the free functions come from here and nowhere else, so the
156
+ * pre-init mistake is not expressible. Destructure at each entry point (route
157
+ * loader, hydration path, worker) rather than threading one result around: the
158
+ * gate is memoized, so every await after the first is free.
159
+ *
116
160
  * Identical in every environment: in a browser the binary is fetched and
117
161
  * streamed, under Node it is read off disk, and the call site is the same line.
118
162
  *
119
- * Idempotent and concurrency-safe: every call returns the same promise, so
120
- * `await init()` at each of several entry points costs one instantiation. A
121
- * failed init clears the memo, so a retry is possible.
163
+ * Idempotent and concurrency-safe: every non-conflicting call returns the same
164
+ * promise, so several entry points cost one instantiation. A failed init clears
165
+ * the memo, so a retry is possible.
166
+ *
167
+ * Both failures reject (§ "Initialization", FAILURE DELIVERY): one `catch`
168
+ * around `await init(...)` covers `runtime::init_conflict` and
169
+ * `runtime::init_failed` alike.
122
170
  *
123
171
  * @param {import('../core/wasm.js').InitInput} [source] override the binary's
124
172
  * source (bytes, a `Response`, a `WebAssembly.Module`, a URL) for hosts that
125
173
  * route assets themselves or embed the binary. Pass it on the FIRST call; a
126
- * later call passing a *different* source throws `runtime::init_conflict`
127
- * rather than silently ignoring it. Passing the same value again is fine, so
128
- * several entry points may each `await init(BYTES)` against one constant.
129
- * @returns {Promise<void>} resolves when the sync surface is usable
174
+ * later call passing a *different* source rejects with
175
+ * `runtime::init_conflict` rather than silently ignoring it. Passing the same
176
+ * value again is fine, so several entry points may each `await init(BYTES)`
177
+ * against one constant.
178
+ * @returns {Promise<import('./runtime.js').CoreSurface>} the core surface, once
179
+ * its instance is live
130
180
  */
131
181
  export function init(source) {
132
182
  if (coreInit) {
133
183
  if (source !== undefined && source !== coreInitSource) {
134
- throw quillmarkError(
135
- 'runtime::init_conflict',
136
- 'init(source): core is already initializing or initialized from a different source.',
137
- 'Pass a source on the first call only, or pass the same value every time.'
184
+ // A rejection, not a throw: this is the one failure on the
185
+ // promise-returning surface that could land on the caller's stack, where
186
+ // `init(BYTES).catch(…)` would not see it.
187
+ return Promise.reject(
188
+ quillmarkError(
189
+ 'runtime::init_conflict',
190
+ 'init(source): core is already initializing or initialized from a different source.',
191
+ 'Pass a source on the first call only, or pass the same value every time.'
192
+ )
138
193
  );
139
194
  }
140
195
  return coreInit;
141
196
  }
142
197
  coreInitSource = source;
143
198
  // Assign before the first await so a synchronous second call sees the memo.
144
- coreInit = instantiateCore(source).catch((err) => {
145
- // Self-heal, as `#resolveBackend` does: one transient failure (a 404, an
146
- // offline fetch) must not poison every later attempt.
147
- coreInit = undefined;
148
- coreInitSource = undefined;
149
- throw err;
150
- });
199
+ coreInit = instantiateCore(source).then(
200
+ () => CORE_SURFACE,
201
+ (err) => {
202
+ // Self-heal, as `#resolveBackend` does: one transient failure (a 404, an
203
+ // offline fetch) must not poison every later attempt.
204
+ coreInit = undefined;
205
+ coreInitSource = undefined;
206
+ throw err;
207
+ }
208
+ );
151
209
  return coreInit;
152
210
  }
153
211
 
package/runtime/uninit.js DELETED
@@ -1,57 +0,0 @@
1
- // The pre-initialization sentinel.
2
- //
3
- // wasm-bindgen's `--target web` glue holds its instance exports in one
4
- // module-level binding (`let wasmModule, wasm;`) that every generated path
5
- // reads through: constructors, statics, methods, free functions. Until
6
- // instantiation assigns it, that binding is `undefined`, so a consumer who
7
- // skipped `await init()` gets `Cannot read properties of undefined (reading
8
- // 'quill_fromTree')` from inside generated code.
9
- //
10
- // `scripts/build-wasm.sh` (`guard_wasm_js`) patches the binding to start as
11
- // this sentinel, so the same access throws a `QuillmarkError` naming the cause
12
- // and the fix. The patch is three line edits per variant, shape-asserted before
13
- // it applies; the logic lives here rather than inside the awk program.
14
- //
15
- // It sits in the generated build, not in the runtime layer, because the
16
- // canonical invariant (`runtime.js` § "CANONICAL INVARIANT") requires the root
17
- // to re-export `Quill`/`Document` VERBATIM: no wrapper, subclass, or proxy may
18
- // stand between a consumer and those classes. Prototype patching preserves
19
- // identity but cannot cover a public constructor (`new Document(...)`) without
20
- // wrapping the class. The binding they all read through is the one place that
21
- // covers every path and touches no class.
22
-
23
- /**
24
- * A stand-in for a wasm build's exports that throws on use. Every property read
25
- * throws except the marker the patched init guards test, and `then`,
26
- * `constructor`, and symbols, which return `undefined`: an incidental `await`
27
- * or `util.inspect` reports nothing rather than throwing somewhere unrelated to
28
- * the cause.
29
- *
30
- * @param {string} message what a consumer sees when they reach a build early
31
- * @param {string} hint the fix, as a `Diagnostic.hint`
32
- * @returns {any} the sentinel
33
- */
34
- export function uninitSentinel(message, hint) {
35
- return new Proxy(
36
- {},
37
- {
38
- get(_target, prop) {
39
- if (prop === UNINIT) return true;
40
- if (prop === 'then' || prop === 'constructor' || typeof prop === 'symbol') {
41
- return undefined;
42
- }
43
- const err = /** @type {any} */ (new Error(message));
44
- err.diagnostics = [
45
- { severity: 'error', code: 'runtime::not_initialized', message, hint }
46
- ];
47
- throw err;
48
- }
49
- }
50
- );
51
- }
52
-
53
- /**
54
- * The marker the patched `if (wasm !== undefined)` guards read to tell a
55
- * sentinel from real instance exports, which never carry it.
56
- */
57
- export const UNINIT = '__quillmarkUninit';