@quillmark/wasm 0.88.0 → 0.90.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 +127 -0
- package/README.md +79 -27
- package/{bundler → backends/typst}/wasm.d.ts +60 -24
- package/{bundler → backends/typst}/wasm_bg.js +119 -55
- package/{bundler → backends/typst}/wasm_bg.wasm +0 -0
- package/{bundler → backends/typst}/wasm_bg.wasm.d.ts +6 -4
- package/core/wasm.d.ts +439 -0
- package/core/wasm.js +9 -0
- package/core/wasm_bg.js +1496 -0
- package/core/wasm_bg.wasm +0 -0
- package/core/wasm_bg.wasm.d.ts +62 -0
- package/package.json +13 -13
- package/runtime/runtime.d.ts +212 -0
- package/runtime/runtime.js +378 -0
- /package/{bundler → backends/typst}/wasm.js +0 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/* @ts-self-types="./runtime.d.ts" */
|
|
2
|
+
//
|
|
3
|
+
// @quillmark/wasm/runtime — the canonical consumer API.
|
|
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`):
|
|
12
|
+
//
|
|
13
|
+
// - `Quill` and `Document` ARE the core build's classes, re-exported. They
|
|
14
|
+
// hold the canonical data and the full sync surface (schema / validate /
|
|
15
|
+
// seed / mutate / toJson / toTree). No backend is loaded to use them, so
|
|
16
|
+
// the editor/validation path never pays for a multi-MB backend binary.
|
|
17
|
+
//
|
|
18
|
+
// - `Engine` is the render dispatcher. It routes on `quill.backendId`, lazily
|
|
19
|
+
// imports that backend's build (so a consumer that never renders never
|
|
20
|
+
// loads it), clones the canonical `Quill`/`Document` into the backend's
|
|
21
|
+
// memory ON DEMAND as data (`toTree`→`fromTree`, `toJson`→`fromJson`),
|
|
22
|
+
// renders, and the backend handles never escape.
|
|
23
|
+
//
|
|
24
|
+
// CLONE LIFETIMES (not all transient): the per-call `Document` clone IS
|
|
25
|
+
// transient — built and freed inside each call, because documents are small
|
|
26
|
+
// and mutate freely. The `Quill` clone is CACHED instead: re-cloning a quill
|
|
27
|
+
// per call means re-serializing its whole file tree Rust→JS, copying it into
|
|
28
|
+
// backend memory, and re-parsing + re-validating the bundle every time — and
|
|
29
|
+
// quills are validated, effectively-immutable bundles. So each `Engine`
|
|
30
|
+
// memoizes the backend-memory quill per (engine, backendId, canonical quill
|
|
31
|
+
// instance) in a `WeakMap` keyed on the canonical `Quill`: when the consumer
|
|
32
|
+
// drops the core quill the cache entry becomes collectable and wasm-bindgen
|
|
33
|
+
// weak-refs (`--weak-refs`) free the backend handle. The CONTRACT this buys:
|
|
34
|
+
// a `Quill` instance's contents never change after construction — mutate by
|
|
35
|
+
// replacing the instance (the clone is dropped with it via WeakMap +
|
|
36
|
+
// weak-refs).
|
|
37
|
+
//
|
|
38
|
+
// The cross-memory crossing is therefore invisible: a consumer hands canonical
|
|
39
|
+
// `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
|
|
40
|
+
|
|
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`).
|
|
48
|
+
//
|
|
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). Keep `Engine` duck-typed on `.toTree()`/`.backendId`/`.toJson()`
|
|
52
|
+
// (it is) so it tolerates handles from any core instance. The `runtime.test.js`
|
|
53
|
+
// "re-exports the internal core build classes verbatim" case
|
|
54
|
+
// (`Quill === CoreQuill`) is the executable guard for this invariant.
|
|
55
|
+
export { Quill, Document, init } from '../core/wasm.js';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Narrow an unknown caught value to a `QuillmarkError` — the error every
|
|
59
|
+
* fallible method in this package throws: a real `Error` with a non-empty
|
|
60
|
+
* `diagnostics` array attached (same entry shape as `RenderResult.warnings`).
|
|
61
|
+
*
|
|
62
|
+
* Structural by necessity AND by design: the WASM layer constructs a plain
|
|
63
|
+
* `Error` and attaches the property (there is no error class to `instanceof`),
|
|
64
|
+
* and a structural check works on errors from any build or WASM instance in
|
|
65
|
+
* the page — consistent with the duck-typed handling of handles elsewhere in
|
|
66
|
+
* this layer.
|
|
67
|
+
*
|
|
68
|
+
* @param {unknown} e
|
|
69
|
+
* @returns {e is Error & { diagnostics: import('../core/wasm.js').Diagnostic[] }}
|
|
70
|
+
*/
|
|
71
|
+
export function isQuillmarkError(e) {
|
|
72
|
+
return e instanceof Error && Array.isArray(/** @type {any} */ (e).diagnostics);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Backend builds are NEVER statically imported here — that would pull a
|
|
76
|
+
// multi-MB binary into the eager graph and defeat lazy loading. Each entry is a
|
|
77
|
+
// DESCRIPTOR: `load` is a thunk returning a dynamic `import()` (a backend's
|
|
78
|
+
// chunk is fetched only when something actually renders against that backend),
|
|
79
|
+
// and `formats`/`canvas` are the REQUIRED static capability manifest so the
|
|
80
|
+
// cheap probes (`supportedFormats`/`supportsCanvas`) ALWAYS answer without
|
|
81
|
+
// loading the binary or cloning the quill. The manifest values are verified
|
|
82
|
+
// against the backend's Rust source (`crates/backends/typst/src/lib.rs`
|
|
83
|
+
// `SUPPORTED_FORMATS` and `supports_canvas`) and pinned by the `runtime.test.js`
|
|
84
|
+
// drift-guard test, which renders once and asserts the loaded backend reports
|
|
85
|
+
// the same list.
|
|
86
|
+
const DEFAULT_BACKENDS = {
|
|
87
|
+
typst: {
|
|
88
|
+
load: () => import('../backends/typst/wasm.js'),
|
|
89
|
+
formats: ['pdf', 'svg', 'png'], // crates/backends/typst/src/lib.rs SUPPORTED_FORMATS
|
|
90
|
+
canvas: true // crates/backends/typst/src/lib.rs supports_canvas() == true
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate a backend registry descriptor, throwing a clear error naming the
|
|
96
|
+
* backend id on any malformed entry. Descriptors are the ONLY accepted form:
|
|
97
|
+
* `{ load, formats, canvas }` with a callable `load`, a `formats` array, and a
|
|
98
|
+
* boolean `canvas`. Failing at construction (not deep inside a render) keeps the
|
|
99
|
+
* capability probes free — they can answer from the manifest unconditionally.
|
|
100
|
+
* @param {string} id
|
|
101
|
+
* @param {unknown} entry
|
|
102
|
+
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
103
|
+
*/
|
|
104
|
+
function validateBackend(id, entry) {
|
|
105
|
+
if (!entry || typeof entry !== 'object') {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Engine: backend '${id}' must be a descriptor { load, formats, canvas }.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const { load, formats, canvas } = /** @type {any} */ (entry);
|
|
111
|
+
if (typeof load !== 'function') {
|
|
112
|
+
throw new Error(`Engine: backend '${id}' descriptor needs a callable 'load'.`);
|
|
113
|
+
}
|
|
114
|
+
if (!Array.isArray(formats)) {
|
|
115
|
+
throw new Error(`Engine: backend '${id}' descriptor needs a 'formats' array.`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof canvas !== 'boolean') {
|
|
118
|
+
throw new Error(`Engine: backend '${id}' descriptor needs a boolean 'canvas'.`);
|
|
119
|
+
}
|
|
120
|
+
return { load, formats, canvas };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Render dispatcher over the canonical `Quill`/`Document`. One `Engine`
|
|
125
|
+
* instance can drive every backend; it resolves the right backend build from
|
|
126
|
+
* each quill's declared `backendId` and loads it lazily on first use.
|
|
127
|
+
*/
|
|
128
|
+
export class Engine {
|
|
129
|
+
/** backendId → Promise<backend module>, memoized so each build loads once. */
|
|
130
|
+
#modules = new Map();
|
|
131
|
+
/** backendId → that backend's engine instance (the WASM backend registry). */
|
|
132
|
+
#engines = new Map();
|
|
133
|
+
/** backendId → descriptor `{ load, formats, canvas }`. */
|
|
134
|
+
#loaders;
|
|
135
|
+
/**
|
|
136
|
+
* backendId → WeakMap<canonical Quill, backend-memory Quill clone>. Caches
|
|
137
|
+
* the expensive quill materialization per (engine, backend, canonical quill
|
|
138
|
+
* instance). WeakMap so dropping the canonical quill makes its clone
|
|
139
|
+
* collectable; the backend handle is then freed by wasm-bindgen weak-refs.
|
|
140
|
+
* @type {Map<string, WeakMap<object, any>>}
|
|
141
|
+
*/
|
|
142
|
+
#quillClones = new Map();
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @param {{ backends?: Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }> }} [options]
|
|
146
|
+
* Extra or overriding backend descriptors, merged over the built-ins. Each
|
|
147
|
+
* entry is a descriptor (`{ load, formats, canvas }`) with `formats` and
|
|
148
|
+
* `canvas` REQUIRED — that static manifest is what makes
|
|
149
|
+
* `supportedFormats`/`supportsCanvas` always free (no binary load, no quill
|
|
150
|
+
* clone). Malformed entries throw here, at construction. The default
|
|
151
|
+
* registry maps `"typst"` to the bundled Typst build.
|
|
152
|
+
*/
|
|
153
|
+
constructor(options) {
|
|
154
|
+
const merged = { ...DEFAULT_BACKENDS, ...(options?.backends ?? {}) };
|
|
155
|
+
/** @type {Record<string, { load: () => Promise<unknown>, formats: string[], canvas: boolean }>} */
|
|
156
|
+
const loaders = {};
|
|
157
|
+
for (const [id, entry] of Object.entries(merged)) {
|
|
158
|
+
loaders[id] = validateBackend(id, entry);
|
|
159
|
+
}
|
|
160
|
+
this.#loaders = loaders;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Look up the registered descriptor for `backendId`, throwing the canonical
|
|
165
|
+
* "no backend registered" error if none. Pure — touches no binary.
|
|
166
|
+
* @param {string} backendId
|
|
167
|
+
* @returns {{ load: () => Promise<unknown>, formats: string[], canvas: boolean }}
|
|
168
|
+
*/
|
|
169
|
+
#descriptorFor(backendId) {
|
|
170
|
+
const descriptor = this.#loaders[backendId];
|
|
171
|
+
if (!descriptor) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Engine: no backend registered for '${backendId}'. ` +
|
|
174
|
+
`Known backends: ${Object.keys(this.#loaders).join(', ') || '(none)'}.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return descriptor;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve (and lazily load) the backend module + its engine for `backendId`.
|
|
182
|
+
* @param {string} backendId
|
|
183
|
+
* @returns {Promise<{ mod: any, engine: any }>}
|
|
184
|
+
*/
|
|
185
|
+
async #resolveBackend(backendId) {
|
|
186
|
+
const descriptor = this.#descriptorFor(backendId);
|
|
187
|
+
|
|
188
|
+
let modPromise = this.#modules.get(backendId);
|
|
189
|
+
if (!modPromise) {
|
|
190
|
+
// Set the promise synchronously (before any await) so concurrent first
|
|
191
|
+
// renders share ONE import. Self-heal on failure so a transient load
|
|
192
|
+
// error doesn't poison every later attempt.
|
|
193
|
+
modPromise = Promise.resolve()
|
|
194
|
+
.then(descriptor.load)
|
|
195
|
+
.catch((err) => {
|
|
196
|
+
this.#modules.delete(backendId);
|
|
197
|
+
throw err;
|
|
198
|
+
});
|
|
199
|
+
this.#modules.set(backendId, modPromise);
|
|
200
|
+
}
|
|
201
|
+
const mod = await modPromise;
|
|
202
|
+
|
|
203
|
+
let engine = this.#engines.get(backendId);
|
|
204
|
+
if (!engine) {
|
|
205
|
+
engine = new mod.Quillmark();
|
|
206
|
+
this.#engines.set(backendId, engine);
|
|
207
|
+
}
|
|
208
|
+
return { mod, engine };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get (or materialize-and-cache) the backend-memory `Quill` clone for
|
|
213
|
+
* `quill` under `backendId`. On a cache miss the clone is built via
|
|
214
|
+
* `toTree`→`fromTree` and stored in the per-backend `WeakMap` keyed on the
|
|
215
|
+
* canonical `Quill` instance, so a later call with the same instance reuses
|
|
216
|
+
* it (the hot-path fix: no re-serialize / re-copy / re-validate per call).
|
|
217
|
+
* @param {any} mod the backend build module
|
|
218
|
+
* @param {string} backendId
|
|
219
|
+
* @param {{ toTree(): Map<string, Uint8Array> }} quill
|
|
220
|
+
* @returns {any} the backend-memory quill clone
|
|
221
|
+
*/
|
|
222
|
+
#cachedQuillClone(mod, backendId, quill) {
|
|
223
|
+
let perQuill = this.#quillClones.get(backendId);
|
|
224
|
+
if (!perQuill) {
|
|
225
|
+
perQuill = new WeakMap();
|
|
226
|
+
this.#quillClones.set(backendId, perQuill);
|
|
227
|
+
}
|
|
228
|
+
let backendQuill = perQuill.get(quill);
|
|
229
|
+
if (!backendQuill) {
|
|
230
|
+
backendQuill = mod.Quill.fromTree(quill.toTree());
|
|
231
|
+
perQuill.set(quill, backendQuill);
|
|
232
|
+
}
|
|
233
|
+
return backendQuill;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Materialize the backend-memory clones for `quill` + `doc` in `backendId`'s
|
|
238
|
+
* memory and run `fn` against the backend engine. Only `render`/`open` call
|
|
239
|
+
* this, so `doc` is always present.
|
|
240
|
+
*
|
|
241
|
+
* Clone lifetimes differ by design: the `doc` clone is TRANSIENT — freed in
|
|
242
|
+
* the `finally` of every call. The `quill` clone is CACHED per (engine,
|
|
243
|
+
* backend, canonical quill instance) and is NOT freed here; a `Quill`
|
|
244
|
+
* instance's contents never change after construction, so it is dropped with
|
|
245
|
+
* the canonical quill (WeakMap collection → wasm-bindgen weak-ref free) when
|
|
246
|
+
* the consumer replaces the instance. A cache miss materializes it once;
|
|
247
|
+
* subsequent calls reuse it.
|
|
248
|
+
* @param {string} backendId
|
|
249
|
+
* @param {{ toTree(): Map<string, Uint8Array> }} quill
|
|
250
|
+
* @param {{ toJson(): string }} doc
|
|
251
|
+
* @param {(ctx: { mod: any, engine: any, quill: any, doc: any }) => any} fn
|
|
252
|
+
*/
|
|
253
|
+
async #withClones(backendId, quill, doc, fn) {
|
|
254
|
+
const { mod, engine } = await this.#resolveBackend(backendId);
|
|
255
|
+
// The quill clone is cached (see #cachedQuillClone); only the per-call doc
|
|
256
|
+
// clone is transient. Bring the doc clone + `fn` under one try so the doc
|
|
257
|
+
// clone is freed even if a later step throws (e.g. `Document.fromJson`
|
|
258
|
+
// rejecting a cross-version DTO). The cached quill clone is intentionally
|
|
259
|
+
// NOT freed here. `fn` MUST be synchronous — the doc clone is freed as soon
|
|
260
|
+
// as it returns, so an async `fn` would have it freed mid-flight.
|
|
261
|
+
const backendQuill = this.#cachedQuillClone(mod, backendId, quill);
|
|
262
|
+
let backendDoc = null;
|
|
263
|
+
try {
|
|
264
|
+
backendDoc = mod.Document.fromJson(doc.toJson());
|
|
265
|
+
return fn({ mod, engine, quill: backendQuill, doc: backendDoc });
|
|
266
|
+
} finally {
|
|
267
|
+
backendDoc?.free();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Render `doc` against `quill` in one shot, returning a `RenderResult`.
|
|
273
|
+
* @param {Quill} quill
|
|
274
|
+
* @param {Document} doc
|
|
275
|
+
* @param {object} [options] render options (`{ format, ppi, pages, producer }`)
|
|
276
|
+
* @returns {Promise<import('./runtime.js').RenderResult>}
|
|
277
|
+
*/
|
|
278
|
+
async render(quill, doc, options) {
|
|
279
|
+
return this.#withClones(quill.backendId, quill, doc, ({ engine, quill: q, doc: d }) =>
|
|
280
|
+
engine.render(q, d, options ?? undefined)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Open an iterative render session (for canvas preview / per-page paint).
|
|
286
|
+
* The session is a self-contained compiled snapshot, so the transient quill
|
|
287
|
+
* and document clones are freed before this returns; the caller owns the
|
|
288
|
+
* returned session and must `.free()` it.
|
|
289
|
+
* @experimental Ships ahead of its first production consumer (the designed
|
|
290
|
+
* canvas live-preview path); the session/paint surface may change in any
|
|
291
|
+
* 0.x release. `render()` is the stable path.
|
|
292
|
+
* @param {Quill} quill
|
|
293
|
+
* @param {Document} doc
|
|
294
|
+
* @returns {Promise<RenderSession>}
|
|
295
|
+
*/
|
|
296
|
+
async open(quill, doc) {
|
|
297
|
+
return this.#withClones(
|
|
298
|
+
quill.backendId,
|
|
299
|
+
quill,
|
|
300
|
+
doc,
|
|
301
|
+
({ engine, quill: q, doc: d }) => new RenderSession(engine.open(q, d))
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The output formats `quill`'s backend can emit. A cheap, non-failing,
|
|
307
|
+
* ALWAYS-free pre-render probe: it answers from the descriptor's required
|
|
308
|
+
* `formats` manifest — NO binary load and NO quill clone — depending only on
|
|
309
|
+
* `quill.backendId`. Stays `async` for API stability (it never awaits a load).
|
|
310
|
+
* @param {Quill} quill
|
|
311
|
+
* @returns {Promise<import('./runtime.js').OutputFormat[]>}
|
|
312
|
+
*/
|
|
313
|
+
async supportedFormats(quill) {
|
|
314
|
+
const descriptor = this.#descriptorFor(quill.backendId);
|
|
315
|
+
// Defensive copy so callers can't mutate the shared manifest.
|
|
316
|
+
return descriptor.formats.slice();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Whether `quill`'s backend can paint sessions to a canvas. Same ALWAYS-free
|
|
321
|
+
* probe as `supportedFormats`: answered from the descriptor's required
|
|
322
|
+
* `canvas` manifest, no load and no clone.
|
|
323
|
+
* @param {Quill} quill
|
|
324
|
+
* @returns {Promise<boolean>}
|
|
325
|
+
*/
|
|
326
|
+
async supportsCanvas(quill) {
|
|
327
|
+
const descriptor = this.#descriptorFor(quill.backendId);
|
|
328
|
+
return descriptor.canvas;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Thin wrapper over a backend's iterative render session. Holds the compiled
|
|
334
|
+
* snapshot; the quill/document it was opened from have already been freed.
|
|
335
|
+
*/
|
|
336
|
+
export class RenderSession {
|
|
337
|
+
/** @param {import('../backends/typst/wasm').RenderSession} inner */
|
|
338
|
+
constructor(inner) {
|
|
339
|
+
this.#inner = inner;
|
|
340
|
+
}
|
|
341
|
+
#inner;
|
|
342
|
+
|
|
343
|
+
get pageCount() {
|
|
344
|
+
return this.#inner.pageCount;
|
|
345
|
+
}
|
|
346
|
+
get backendId() {
|
|
347
|
+
return this.#inner.backendId;
|
|
348
|
+
}
|
|
349
|
+
get supportsCanvas() {
|
|
350
|
+
return this.#inner.supportsCanvas;
|
|
351
|
+
}
|
|
352
|
+
get warnings() {
|
|
353
|
+
return this.#inner.warnings;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** @param {object} [options] */
|
|
357
|
+
render(options) {
|
|
358
|
+
return this.#inner.render(options ?? undefined);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** @param {number} page */
|
|
362
|
+
pageSize(page) {
|
|
363
|
+
return this.#inner.pageSize(page);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @param {CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D} ctx
|
|
368
|
+
* @param {number} page
|
|
369
|
+
* @param {object} [options]
|
|
370
|
+
*/
|
|
371
|
+
paint(ctx, page, options) {
|
|
372
|
+
return this.#inner.paint(ctx, page, options);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
free() {
|
|
376
|
+
this.#inner.free();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
File without changes
|