@quillmark/wasm 0.81.0-rc.1 → 0.81.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 CHANGED
@@ -69,7 +69,7 @@ semantic equality.
69
69
 
70
70
  ### `doc.toJson()`
71
71
  Serialize the document to a versioned storage DTO — a JSON **string**
72
- carrying a `schema` version tag. Use this (not `toMarkdown`) to persist a
72
+ carrying a `schema` version. Use this (not `toMarkdown`) to persist a
73
73
  document across a process restart or crate upgrade: the wire format is
74
74
  frozen per `schema` version, whereas Markdown syntax evolves. Parse-time
75
75
  `warnings` are not part of the DTO.
@@ -79,6 +79,13 @@ global is not involved. It is standard JSON text, so callers may
79
79
  `JSON.parse` it to inspect it — but it is intended as an opaque blob you
80
80
  persist and hand back.
81
81
 
82
+ `toJson()` is **deterministic**: a `Document` that is `equals` to another
83
+ serializes to a byte-identical string — across repeated calls, and across
84
+ any crate upgrade that keeps the same `schema` version (every release does until
85
+ the `Document` model changes; see [Storage compatibility](#storage-compatibility-across-versions)).
86
+ Field order is fixed and object key order is preserved, so content hashes
87
+ and string-equality dirty-checks over the output are stable.
88
+
82
89
  ### `Document.fromJson(json)`
83
90
  Reconstruct a `Document` from a storage DTO string produced by `toJson`.
84
91
  Round-trips losslessly:
@@ -89,9 +96,57 @@ const restored = Document.fromJson(stored);
89
96
  restored.equals(doc); // true
90
97
  ```
91
98
 
92
- Throws a JS `Error` on malformed JSON, an unknown `schema` tag, or a
99
+ Throws a JS `Error` on malformed JSON, an unknown `schema` version, or a
93
100
  malformed payload. The restored document has no parse-time `warnings`.
94
101
 
102
+ ### `Document.tryFromJson(json)`
103
+ Like `fromJson`, but returns `undefined` instead of throwing when `json` is
104
+ not a valid storage DTO. Use it to branch on format without a heuristic or
105
+ `try`/`catch` as control flow:
106
+
107
+ ```ts
108
+ // "JSON canonical, Markdown fallback" — no exceptions, no string sniffing
109
+ const doc = Document.tryFromJson(content) ?? Document.fromMarkdown(content);
110
+ ```
111
+
112
+ `undefined` means only "not a storage DTO"; `fromMarkdown` still throws on
113
+ genuinely malformed Markdown.
114
+
115
+ ### Storage compatibility across versions
116
+
117
+ The `schema` value (`quillmark/document@0.81.0`) is the **model version**,
118
+ not the running crate version. It is a hand-set constant, bumped only when
119
+ the `Document` model itself changes — so every `0.81.x` patch release reads
120
+ and writes that same value.
121
+
122
+ - **Upgrading is safe.** A newer build always reads documents written by an
123
+ older one. Each schema version's wire format is frozen and never changes;
124
+ when the model does change, the new build ships a migration that converts
125
+ old payloads on `fromJson`. A document you commit as your canonical
126
+ on-disk format keeps loading across crate upgrades — there is no need to
127
+ pin old wasm to read old data.
128
+ - **Downgrading is not.** `fromJson` rejects an *unknown* (i.e. newer)
129
+ `schema` version rather than guessing at a format it predates. Don't feed
130
+ documents written by a newer build back into an older one.
131
+
132
+ To detect a version mismatch before parsing, use the static accessors:
133
+
134
+ ```ts
135
+ const v = Document.schemaVersionOf(blob); // undefined | string
136
+ if (v && v !== Document.currentSchemaVersion()) {
137
+ // payload is from a build with a different model version
138
+ }
139
+ ```
140
+
141
+ `schemaVersionOf` does not validate the payload — it only reads the
142
+ `schema` field, returning `undefined` for non-JSON, non-objects, or
143
+ payloads that don't carry one. Use it to distinguish "wrong version" from
144
+ "corrupt" when `fromJson` throws.
145
+
146
+ In short: persist the `toJson` string, upgrade freely, never downgrade. The
147
+ full design — including how migrations are added — is in
148
+ `prose/canon/DOCUMENT_STORAGE.md`.
149
+
95
150
  ### `doc.equals(other)`
96
151
  Structural equality between two `Document` handles. Compares `main` and
97
152
  `cards` by value; parse-time `warnings` are intentionally excluded.
@@ -218,8 +273,11 @@ compilation failures. The same shape applies to every throw site:
218
273
  The wasm bindings are built with `--weak-refs`, so dropped `Document`,
219
274
  `Quill`, and `RenderSession` handles are reclaimed by `FinalizationRegistry`
220
275
  without manual `.free()` discipline. `.free()` is still emitted as an eager
221
- teardown hook for callers that want deterministic release. Requires
222
- Node 14.6+ / current evergreen browsers (all supported targets).
276
+ teardown hook for callers that want deterministic release.
277
+
278
+ The package requires Node 24+ (`engines: { node: ">=24" }`) and current
279
+ evergreen browsers; `--weak-refs` itself only needs Node 14.6+, but the
280
+ published package floor is 24.
223
281
 
224
282
  For environments where `using` (the [explicit resource management][erm]
225
283
  proposal) hasn't landed, use an explicit `try` / `finally`:
package/bundler/wasm.d.ts CHANGED
@@ -269,6 +269,20 @@ export class Document {
269
269
  * history of either handle.
270
270
  */
271
271
  clone(): Document;
272
+ /**
273
+ * Schema version this build writes via [`toJson`](Document::to_json).
274
+ *
275
+ * Compare a payload's [`schemaVersionOf`](Document::schema_version_of)
276
+ * against this to detect mismatches before calling
277
+ * [`fromJson`](Document::from_json).
278
+ *
279
+ * The tag tracks the **`Document` model version** — the crate version
280
+ * at which the wire format was last changed — not the running crate
281
+ * version. Every patch and minor release within the same model
282
+ * generation returns the same string; the tag advances only when a
283
+ * new schema variant ships.
284
+ */
285
+ static currentSchemaVersion(): string;
272
286
  /**
273
287
  * Structural equality against another `Document`.
274
288
  *
@@ -364,6 +378,27 @@ export class Document {
364
378
  * Mutators never modify `warnings`.
365
379
  */
366
380
  replaceBody(body: string): void;
381
+ /**
382
+ * Read the schema version from a raw storage DTO string without
383
+ * performing a full parse, or `undefined`.
384
+ *
385
+ * Returns the `schema` field as-is — including unknown future versions
386
+ * that `fromJson` would reject. Use this to distinguish "this build is
387
+ * too old for the payload" from "the payload is corrupt" when
388
+ * [`fromJson`](Document::from_json) throws:
389
+ *
390
+ * ```js
391
+ * const v = Document.schemaVersionOf(blob);
392
+ * if (v === undefined) {
393
+ * // not a stored DTO at all — try fromMarkdown
394
+ * } else if (v !== Document.currentSchemaVersion()) {
395
+ * // newer (or older unmigrated) schema — prompt the user to upgrade
396
+ * } else {
397
+ * doc = Document.fromJson(blob);
398
+ * }
399
+ * ```
400
+ */
401
+ static schemaVersionOf(json: string): string | undefined;
367
402
  /**
368
403
  * Replace the tag of the composable card at `index`.
369
404
  *
@@ -413,7 +448,7 @@ export class Document {
413
448
  * Serialize this document to a versioned storage DTO string.
414
449
  *
415
450
  * Returns the document as a JSON string carrying a `schema` version
416
- * tag. The string is produced inside the module via `serde_json` and
451
+ * field. The string is produced inside the module via `serde_json` and
417
452
  * round-trips losslessly back to an equal `Document` via
418
453
  * [`fromJson`](Document::from_json) — the JS `JSON` global is not
419
454
  * involved in either direction.
@@ -426,6 +461,22 @@ export class Document {
426
461
  * The result is standard JSON text, so callers that want to inspect it
427
462
  * may `JSON.parse` it — but treating it as an opaque blob is the
428
463
  * intended use.
464
+ *
465
+ * ## Byte-stability
466
+ *
467
+ * The output is a **byte-deterministic** function of the document's
468
+ * value within a given `schema` version: two documents that compare
469
+ * equal under [`equals`](Document::equals) serialize to byte-equal
470
+ * strings, and the same document re-serialized in a later patch or
471
+ * minor release of this crate (same `schema`) produces the same bytes.
472
+ * Content-hash use cases (template-divergence detection, cache keys)
473
+ * can rely on this without re-canonicalizing.
474
+ *
475
+ * Specifics: object fields are emitted in struct-declaration order;
476
+ * frontmatter field values preserve their YAML insertion order (no
477
+ * key sorting); whitespace is `serde_json`'s compact form (no spaces
478
+ * between tokens, no trailing newline); strings use `serde_json`'s
479
+ * standard escape set. A schema-version bump may change any of these.
429
480
  */
430
481
  toJson(): string;
431
482
  /**
@@ -436,6 +487,21 @@ export class Document {
436
487
  * produces a `Document` equal to `self` by value and by type.
437
488
  */
438
489
  toMarkdown(): string;
490
+ /**
491
+ * Reconstruct a `Document` from a storage DTO string, or `undefined`.
492
+ *
493
+ * Like [`fromJson`](Document::from_json), but returns `undefined`
494
+ * instead of throwing when `json` is not a valid storage DTO. Use this
495
+ * to detect format and fall back without exceptions as control flow:
496
+ *
497
+ * ```js
498
+ * const doc = Document.tryFromJson(content) ?? Document.fromMarkdown(content);
499
+ * ```
500
+ *
501
+ * `undefined` only ever means "not a storage DTO" — `fromMarkdown`
502
+ * still throws on genuinely malformed markdown.
503
+ */
504
+ static tryFromJson(json: string): Document | undefined;
439
505
  /**
440
506
  * Replace the body of the card at `index`.
441
507
  *
@@ -61,6 +61,36 @@ export class Document {
61
61
  const ret = wasm.document_clone(this.__wbg_ptr);
62
62
  return Document.__wrap(ret);
63
63
  }
64
+ /**
65
+ * Schema version this build writes via [`toJson`](Document::to_json).
66
+ *
67
+ * Compare a payload's [`schemaVersionOf`](Document::schema_version_of)
68
+ * against this to detect mismatches before calling
69
+ * [`fromJson`](Document::from_json).
70
+ *
71
+ * The tag tracks the **`Document` model version** — the crate version
72
+ * at which the wire format was last changed — not the running crate
73
+ * version. Every patch and minor release within the same model
74
+ * generation returns the same string; the tag advances only when a
75
+ * new schema variant ships.
76
+ * @returns {string}
77
+ */
78
+ static currentSchemaVersion() {
79
+ let deferred1_0;
80
+ let deferred1_1;
81
+ try {
82
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
83
+ wasm.document_currentSchemaVersion(retptr);
84
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
85
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
86
+ deferred1_0 = r0;
87
+ deferred1_1 = r1;
88
+ return getStringFromWasm0(r0, r1);
89
+ } finally {
90
+ wasm.__wbindgen_add_to_stack_pointer(16);
91
+ wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);
92
+ }
93
+ }
64
94
  /**
65
95
  * Structural equality against another `Document`.
66
96
  *
@@ -320,6 +350,46 @@ export class Document {
320
350
  const len0 = WASM_VECTOR_LEN;
321
351
  wasm.document_replaceBody(this.__wbg_ptr, ptr0, len0);
322
352
  }
353
+ /**
354
+ * Read the schema version from a raw storage DTO string without
355
+ * performing a full parse, or `undefined`.
356
+ *
357
+ * Returns the `schema` field as-is — including unknown future versions
358
+ * that `fromJson` would reject. Use this to distinguish "this build is
359
+ * too old for the payload" from "the payload is corrupt" when
360
+ * [`fromJson`](Document::from_json) throws:
361
+ *
362
+ * ```js
363
+ * const v = Document.schemaVersionOf(blob);
364
+ * if (v === undefined) {
365
+ * // not a stored DTO at all — try fromMarkdown
366
+ * } else if (v !== Document.currentSchemaVersion()) {
367
+ * // newer (or older unmigrated) schema — prompt the user to upgrade
368
+ * } else {
369
+ * doc = Document.fromJson(blob);
370
+ * }
371
+ * ```
372
+ * @param {string} json
373
+ * @returns {string | undefined}
374
+ */
375
+ static schemaVersionOf(json) {
376
+ try {
377
+ const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
378
+ const ptr0 = passStringToWasm0(json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
379
+ const len0 = WASM_VECTOR_LEN;
380
+ wasm.document_schemaVersionOf(retptr, ptr0, len0);
381
+ var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
382
+ var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
383
+ let v2;
384
+ if (r0 !== 0) {
385
+ v2 = getStringFromWasm0(r0, r1).slice();
386
+ wasm.__wbindgen_export4(r0, r1 * 1, 1);
387
+ }
388
+ return v2;
389
+ } finally {
390
+ wasm.__wbindgen_add_to_stack_pointer(16);
391
+ }
392
+ }
323
393
  /**
324
394
  * Replace the tag of the composable card at `index`.
325
395
  *
@@ -432,7 +502,7 @@ export class Document {
432
502
  * Serialize this document to a versioned storage DTO string.
433
503
  *
434
504
  * Returns the document as a JSON string carrying a `schema` version
435
- * tag. The string is produced inside the module via `serde_json` and
505
+ * field. The string is produced inside the module via `serde_json` and
436
506
  * round-trips losslessly back to an equal `Document` via
437
507
  * [`fromJson`](Document::from_json) — the JS `JSON` global is not
438
508
  * involved in either direction.
@@ -445,30 +515,38 @@ export class Document {
445
515
  * The result is standard JSON text, so callers that want to inspect it
446
516
  * may `JSON.parse` it — but treating it as an opaque blob is the
447
517
  * intended use.
518
+ *
519
+ * ## Byte-stability
520
+ *
521
+ * The output is a **byte-deterministic** function of the document's
522
+ * value within a given `schema` version: two documents that compare
523
+ * equal under [`equals`](Document::equals) serialize to byte-equal
524
+ * strings, and the same document re-serialized in a later patch or
525
+ * minor release of this crate (same `schema`) produces the same bytes.
526
+ * Content-hash use cases (template-divergence detection, cache keys)
527
+ * can rely on this without re-canonicalizing.
528
+ *
529
+ * Specifics: object fields are emitted in struct-declaration order;
530
+ * frontmatter field values preserve their YAML insertion order (no
531
+ * key sorting); whitespace is `serde_json`'s compact form (no spaces
532
+ * between tokens, no trailing newline); strings use `serde_json`'s
533
+ * standard escape set. A schema-version bump may change any of these.
448
534
  * @returns {string}
449
535
  */
450
536
  toJson() {
451
- let deferred2_0;
452
- let deferred2_1;
537
+ let deferred1_0;
538
+ let deferred1_1;
453
539
  try {
454
540
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
455
541
  wasm.document_toJson(retptr, this.__wbg_ptr);
456
542
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
457
543
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
458
- var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
459
- var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
460
- var ptr1 = r0;
461
- var len1 = r1;
462
- if (r3) {
463
- ptr1 = 0; len1 = 0;
464
- throw takeObject(r2);
465
- }
466
- deferred2_0 = ptr1;
467
- deferred2_1 = len1;
468
- return getStringFromWasm0(ptr1, len1);
544
+ deferred1_0 = r0;
545
+ deferred1_1 = r1;
546
+ return getStringFromWasm0(r0, r1);
469
547
  } finally {
470
548
  wasm.__wbindgen_add_to_stack_pointer(16);
471
- wasm.__wbindgen_export4(deferred2_0, deferred2_1, 1);
549
+ wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);
472
550
  }
473
551
  }
474
552
  /**
@@ -495,6 +573,28 @@ export class Document {
495
573
  wasm.__wbindgen_export4(deferred1_0, deferred1_1, 1);
496
574
  }
497
575
  }
576
+ /**
577
+ * Reconstruct a `Document` from a storage DTO string, or `undefined`.
578
+ *
579
+ * Like [`fromJson`](Document::from_json), but returns `undefined`
580
+ * instead of throwing when `json` is not a valid storage DTO. Use this
581
+ * to detect format and fall back without exceptions as control flow:
582
+ *
583
+ * ```js
584
+ * const doc = Document.tryFromJson(content) ?? Document.fromMarkdown(content);
585
+ * ```
586
+ *
587
+ * `undefined` only ever means "not a storage DTO" — `fromMarkdown`
588
+ * still throws on genuinely malformed markdown.
589
+ * @param {string} json
590
+ * @returns {Document | undefined}
591
+ */
592
+ static tryFromJson(json) {
593
+ const ptr0 = passStringToWasm0(json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
594
+ const len0 = WASM_VECTOR_LEN;
595
+ const ret = wasm.document_tryFromJson(ptr0, len0);
596
+ return ret === 0 ? undefined : Document.__wrap(ret);
597
+ }
498
598
  /**
499
599
  * Replace the body of the card at `index`.
500
600
  *
Binary file
@@ -8,6 +8,7 @@ export const __wbg_rendersession_free: (a: number, b: number) => void;
8
8
  export const document_cardCount: (a: number) => number;
9
9
  export const document_cards: (a: number) => number;
10
10
  export const document_clone: (a: number) => number;
11
+ export const document_currentSchemaVersion: (a: number) => void;
11
12
  export const document_equals: (a: number, b: number) => number;
12
13
  export const document_fromJson: (a: number, b: number, c: number) => void;
13
14
  export const document_fromMarkdown: (a: number, b: number, c: number) => void;
@@ -20,12 +21,14 @@ export const document_removeCard: (a: number, b: number) => number;
20
21
  export const document_removeCardField: (a: number, b: number, c: number, d: number, e: number) => void;
21
22
  export const document_removeField: (a: number, b: number, c: number, d: number) => void;
22
23
  export const document_replaceBody: (a: number, b: number, c: number) => void;
24
+ export const document_schemaVersionOf: (a: number, b: number, c: number) => void;
23
25
  export const document_setCardTag: (a: number, b: number, c: number, d: number, e: number) => void;
24
26
  export const document_setField: (a: number, b: number, c: number, d: number, e: number) => void;
25
27
  export const document_setFill: (a: number, b: number, c: number, d: number, e: number) => void;
26
28
  export const document_setQuillRef: (a: number, b: number, c: number, d: number) => void;
27
29
  export const document_toJson: (a: number, b: number) => void;
28
30
  export const document_toMarkdown: (a: number, b: number) => void;
31
+ export const document_tryFromJson: (a: number, b: number) => number;
29
32
  export const document_updateCardBody: (a: number, b: number, c: number, d: number, e: number) => void;
30
33
  export const document_updateCardField: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
31
34
  export const document_warnings: (a: number) => number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quillmark/wasm",
3
- "version": "0.81.0-rc.1",
3
+ "version": "0.81.0",
4
4
  "description": "WebAssembly bindings for quillmark",
5
5
  "type": "module",
6
6
  "license": "MIT OR Apache-2.0",