| 1 | // Generates the vectors that hold the Rust core to this module's result |
| 2 | // (spec §2.8, Docs' rule adopted unchanged). For each data/<name>.json — |
| 3 | // {guid, ops} — a fresh Y.Doc with that guid and clientID 1 takes the |
| 4 | // ops through applyOps; vectors/<name>.yjs is the whole state as a v1 |
| 5 | // update and vectors/<name>.view.json is this side's view of it. |
| 6 | // internal/slides/core/vectors_test.go loads each .yjs through the core |
| 7 | // and compares views, and applies the same ops through the core's own |
| 8 | // Ops and compares again. |
| 9 | // |
| 10 | // These are the contract: never edit a vector to make a test pass. A |
| 11 | // deliberate rule change regenerates them in the same commit. The ids |
| 12 | // come from the data files so a run is the same bytes every time. |
| 13 | import * as Y from 'yjs' |
| 14 | import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' |
| 15 | import { join, dirname } from 'node:path' |
| 16 | import { fileURLToPath } from 'node:url' |
| 17 | import { applyOps, view } from './ops.mjs' |
| 18 | import { checkDoc } from './check.mjs' |
| 19 | |
| 20 | const here = dirname(fileURLToPath(import.meta.url)) |
| 21 | export const dataDir = join(here, 'data') |
| 22 | export const vectorsDir = join(here, 'vectors') |
| 23 | |
| 24 | /** JSON with every object's keys sorted, pretty-printed, newline-terminated. */ |
| 25 | export function sortedJSON (v) { |
| 26 | const sort = x => { |
| 27 | if (Array.isArray(x)) return x.map(sort) |
| 28 | if (x !== null && typeof x === 'object') { |
| 29 | const out = {} |
| 30 | for (const k of Object.keys(x).sort()) out[k] = sort(x[k]) |
| 31 | return out |
| 32 | } |
| 33 | return x |
| 34 | } |
| 35 | return JSON.stringify(sort(v), null, 2) + '\n' |
| 36 | } |
| 37 | |
| 38 | /** The update and view for one data file's ops. */ |
| 39 | export function buildVector (name, data) { |
| 40 | if (data.guid !== name) throw new Error(`${name}.json: guid ${JSON.stringify(data.guid)} must equal the file name`) |
| 41 | const doc = new Y.Doc({ guid: data.guid }) |
| 42 | doc.clientID = 1 |
| 43 | applyOps(doc, data.ops) |
| 44 | // What the ops write must pass the accepted-shape check the server |
| 45 | // gates a posted update on (check.mjs, core/src/check.rs) — a vector |
| 46 | // that did not would be a contract the wire refuses. |
| 47 | const problem = checkDoc(doc) |
| 48 | if (problem) throw new Error(`${name}: the ops produced text outside the schema: ${problem}`) |
| 49 | return { update: Y.encodeStateAsUpdate(doc), view: view(doc) } |
| 50 | } |
| 51 | |
| 52 | export function generate () { |
| 53 | mkdirSync(vectorsDir, { recursive: true }) |
| 54 | const names = readdirSync(dataDir).filter(f => f.endsWith('.json')).map(f => f.slice(0, -5)).sort() |
| 55 | for (const name of names) { |
| 56 | const data = JSON.parse(readFileSync(join(dataDir, `${name}.json`), 'utf8')) |
| 57 | const { update, view: v } = buildVector(name, data) |
| 58 | writeFileSync(join(vectorsDir, `${name}.yjs`), Buffer.from(update)) |
| 59 | writeFileSync(join(vectorsDir, `${name}.view.json`), sortedJSON(v)) |
| 60 | console.log(`${name}: ${data.ops.length} ops, ${update.length} bytes`) |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) generate() |
| 65 | |