@quillmark/wasm 0.102.0 → 0.104.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.
@@ -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,135 @@ 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
- // the surface it guards cannot be async: `Quill.fromTree` and `seedDocument`
95
- // are sync and static, so there is nowhere to hide an await except in front.
93
+ // the surface it guards cannot be async: `Quill.fromTree` and
94
+ // `quill.seedDocument` return synchronously, so there is nowhere to hide an
95
+ // await except in front.
96
+ //
97
+ // WHAT STAYS A STATIC EXPORT is what needs no instance. `MAIN_CARD_ADDR`, the
98
+ // open-set guards and `isQuillmarkError` are pure JS over plain objects; gating
99
+ // them would cost a consumer of one an await it has no use for.
96
100
  //
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).
101
+ // `Engine`, `LiveSession` and the four writer/reader classes stay static too,
102
+ // gated by their ARGUMENTS rather than by the door. Every `Engine` verb takes a
103
+ // `Quill` first (`#backendOf` is the single reader) and the writer/reader
104
+ // constructors take both handles, so a caller who has not awaited cannot
105
+ // produce an argument to call them with. The two constructors taking no handle
106
+ // reach no wasm: `new Engine()` validates a descriptor map, and a `LiveSession`
107
+ // forwards to the backend session `engine.open` is the sole source of. None of
108
+ // the six carries a static method, the one member shape an argument cannot
109
+ // gate. `gate.test.js` is the executable guard, driving the whole static
110
+ // surface before `init`. Holding them out of the gate keeps them tree-shakable,
111
+ // so the editor path drops the dispatcher it never calls.
112
+ //
113
+ // FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
114
+ // throws, a promise-returning verb rejects, and nothing does both. A
115
+ // programming error reached through a promise-returning verb
116
+ // (`runtime::foreign_handle` inside `Engine.render`) rejects like any other.
117
+ // `init` is the one promise-returning export not declared `async`, because the
118
+ // memo is returned by identity; its conflict guard rejects explicitly to hold
119
+ // the rule, which the return type cannot declare and a `.catch` would not see.
100
120
 
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. */
121
+ /**
122
+ * The gated surface: the core build's values, which are exactly the ones its
123
+ * instance stands behind. Frozen and built at module scope, because the classes
124
+ * and functions themselves resolve synchronously (only the instance behind them
125
+ * is late), so there is nothing to defer and no per-call allocation.
126
+ *
127
+ * Membership is derived, not chosen: it is the core build's exports minus its
128
+ * instantiation machinery (`default`, `initSync`, and the start-section
129
+ * `start`), which `init` owns and no consumer calls. `init.test.js` § "the
130
+ * gated surface" computes that set and pins it, so a new core export that never
131
+ * reaches here fails there rather than going missing.
132
+ * @type {import('./runtime.js').CoreSurface}
133
+ */
134
+ const CORE_SURFACE = Object.freeze({
135
+ Quill,
136
+ Document,
137
+ importMarkdown,
138
+ exportMarkdown,
139
+ rebase,
140
+ mapPos,
141
+ parseDocPath,
142
+ formatDocPath
143
+ });
144
+
145
+ /** The in-flight or settled core instantiation, resolving to `CORE_SURFACE`.
146
+ * The memo is the PROMISE, not a boolean, so concurrent callers share one
147
+ * instantiation instead of racing.
148
+ * @type {Promise<import('./runtime.js').CoreSurface> | undefined} */
103
149
  let coreInit;
104
150
  /** The source `init` was first called with; the conflict check reads it. */
105
151
  let coreInitSource;
106
152
 
107
153
  /**
108
- * Instantiate the core WASM build. Call once at startup, before any other
109
- * export is used; extra calls are free.
154
+ * Instantiate the core WASM build and resolve to its surface.
110
155
  *
111
156
  * ```js
112
- * import { init, Quill, Engine } from '@quillmark/wasm';
113
- * await init();
157
+ * import { init } from '@quillmark/wasm';
158
+ * const { Quill, Document } = await init();
114
159
  * ```
115
160
  *
161
+ * The classes and the free functions come from here and nowhere else, so the
162
+ * pre-init mistake is not expressible. Destructure at each entry point (route
163
+ * loader, hydration path, worker) rather than threading one result around: the
164
+ * gate is memoized, so every await after the first is free.
165
+ *
116
166
  * Identical in every environment: in a browser the binary is fetched and
117
167
  * streamed, under Node it is read off disk, and the call site is the same line.
118
168
  *
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.
169
+ * Idempotent and concurrency-safe: every non-conflicting call returns the same
170
+ * promise, so several entry points cost one instantiation. A failed init clears
171
+ * the memo, so a retry is possible.
172
+ *
173
+ * Both failures reject (§ "Initialization", FAILURE DELIVERY): one `catch`
174
+ * around `await init(...)` covers `runtime::init_conflict` and
175
+ * `runtime::init_failed` alike.
122
176
  *
123
177
  * @param {import('../core/wasm.js').InitInput} [source] override the binary's
124
178
  * source (bytes, a `Response`, a `WebAssembly.Module`, a URL) for hosts that
125
179
  * 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
180
+ * later call passing a *different* source rejects with
181
+ * `runtime::init_conflict` rather than silently ignoring it. Passing the same
182
+ * value again is fine, so several entry points may each `await init(BYTES)`
183
+ * against one constant.
184
+ * @returns {Promise<import('./runtime.js').CoreSurface>} the core surface, once
185
+ * its instance is live
130
186
  */
131
187
  export function init(source) {
132
188
  if (coreInit) {
133
189
  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.'
190
+ // A rejection, not a throw: this is the one failure on the
191
+ // promise-returning surface that could land on the caller's stack, where
192
+ // `init(BYTES).catch(…)` would not see it.
193
+ return Promise.reject(
194
+ quillmarkError(
195
+ 'runtime::init_conflict',
196
+ 'init(source): core is already initializing or initialized from a different source.',
197
+ 'Pass a source on the first call only, or pass the same value every time.'
198
+ )
138
199
  );
139
200
  }
140
201
  return coreInit;
141
202
  }
142
203
  coreInitSource = source;
143
204
  // 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
- });
205
+ coreInit = instantiateCore(source).then(
206
+ () => CORE_SURFACE,
207
+ (err) => {
208
+ // Self-heal, as `#resolveBackend` does: one transient failure (a 404, an
209
+ // offline fetch) must not poison every later attempt.
210
+ coreInit = undefined;
211
+ coreInitSource = undefined;
212
+ throw err;
213
+ }
214
+ );
151
215
  return coreInit;
152
216
  }
153
217
 
@@ -452,9 +516,10 @@ export function isListItemContainer(container) {
452
516
  // A predicate rather than an exported name list, because the known tables below
453
517
  // are upstream's business. They are pinned against the Rust source
454
518
  // (`Content::RESERVED_*` and `KnownIslandType`) by the
455
- // `known_open_set_names_are_pinned` drift-guard test in
456
- // `crates/content/src/model.rs`: adding a built-in means editing there, here, and
457
- // the TS unions in `crates/bindings/wasm/src/engine.rs` in one commit.
519
+ // `js_known_name_tables_match_the_rust_open_sets` drift-guard test in
520
+ // `crates/bindings/wasm/tests/known_names_drift.rs`: adding a built-in means
521
+ // editing there, here, and the TS unions in `crates/bindings/wasm/src/engine.rs`
522
+ // in one commit.
458
523
  //
459
524
  // These classify unknown *tags*, not unknown *payloads on known tags*. A future
460
525
  // `kind: "footnote"` with a sibling `ref` loses `ref` at a consumer that predates
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';