| 1 | // Generates the Yjs wire fixtures that define compatibility for the Rust |
| 2 | // core. These are the contract: never edit a fixture to make a test pass. |
| 3 | // A deliberate rule change regenerates them in the same commit. |
| 4 | import * as Y from 'yjs' |
| 5 | import { mkdirSync, writeFileSync } from 'node:fs' |
| 6 | import { join, dirname } from 'node:path' |
| 7 | import { fileURLToPath } from 'node:url' |
| 8 | |
| 9 | const dataDir = join(dirname(fileURLToPath(import.meta.url)), 'data') |
| 10 | mkdirSync(dataDir, { recursive: true }) |
| 11 | |
| 12 | function emit(name, build) { |
| 13 | const doc = new Y.Doc() |
| 14 | // new Y.Doc() assigns a random clientID. Fixed IDs ensure that running the |
| 15 | // generator twice produces byte-identical fixtures (required for the |
| 16 | // regeneration gate that later tasks add). |
| 17 | doc.clientID = 1 |
| 18 | build(doc) |
| 19 | const update = Y.encodeStateAsUpdate(doc) |
| 20 | writeFileSync(join(dataDir, `${name}.update.bin`), Buffer.from(update)) |
| 21 | writeFileSync( |
| 22 | join(dataDir, `${name}.expected.json`), |
| 23 | JSON.stringify({ |
| 24 | text: doc.getText('content').toString(), |
| 25 | delta: doc.getText('content').toDelta() |
| 26 | }, null, 2) + '\n' |
| 27 | ) |
| 28 | console.log(`${name}: ${update.length} bytes`) |
| 29 | } |
| 30 | |
| 31 | emit('hello', (doc) => { |
| 32 | doc.getText('content').insert(0, 'hello world') |
| 33 | }) |
| 34 | |
| 35 | emit('unicode', (doc) => { |
| 36 | // Surrogate pairs and combining marks: Yjs indexes UTF-16 code units and |
| 37 | // Rust indexes UTF-8 bytes, so this is where an index-conversion bug shows. |
| 38 | doc.getText('content').insert(0, 'héllo 🌍 café') |
| 39 | }) |
| 40 | |
| 41 | emit('formatted', (doc) => { |
| 42 | // Rich text formatting: bold is the key assertion. The delta field captures |
| 43 | // the formatting structure that toString() would strip, verifying that the |
| 44 | // Rust side correctly decodes format operations. |
| 45 | const t = doc.getText('content') |
| 46 | t.insert(0, 'plain bold plain') |
| 47 | t.format(6, 4, { bold: true }) |
| 48 | }) |
| 49 | |
| 50 | emit('concurrent', (doc) => { |
| 51 | // Two docs diverge and merge. The merged state is order-independent, so |
| 52 | // Rust must arrive at the same bytes from the same inputs. |
| 53 | const a = new Y.Doc() |
| 54 | a.clientID = 1 |
| 55 | const b = new Y.Doc() |
| 56 | b.clientID = 2 |
| 57 | // Note: doc.clientID (set to 3 here) overrides the emit() assignment above. |
| 58 | // The clientID of this local doc must differ from a and b to ensure |
| 59 | // deterministic merge ordering. |
| 60 | doc.clientID = 3 |
| 61 | a.getText('content').insert(0, 'AAA') |
| 62 | b.getText('content').insert(0, 'BBB') |
| 63 | Y.applyUpdate(a, Y.encodeStateAsUpdate(b)) |
| 64 | Y.applyUpdate(b, Y.encodeStateAsUpdate(a)) |
| 65 | Y.applyUpdate(doc, Y.encodeStateAsUpdate(a)) |
| 66 | }) |
| 67 | |