Playing with Y.js

This commit is contained in:
2025-08-09 19:53:00 -07:00
parent 128800eda8
commit 19a5a7bae6
3 changed files with 136 additions and 0 deletions
+66
View File
@@ -8,7 +8,12 @@
"name": "story-editor", "name": "story-editor",
"version": "0.1.0", "version": "0.1.0",
"license": "ISC", "license": "ISC",
"dependencies": {
"lodash": "^4.17.21",
"yjs": "^13.6.27"
},
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.20",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"vite": "^7.1.1" "vite": "^7.1.1"
@@ -743,6 +748,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.8", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
@@ -815,6 +827,43 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lib0": {
"version": "0.2.114",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT",
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -1054,6 +1103,23 @@
"optional": true "optional": true
} }
} }
},
"node_modules/yjs": {
"version": "13.6.27",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
"license": "MIT",
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
} }
} }
} }
+5
View File
@@ -13,8 +13,13 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.20",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"vite": "^7.1.1" "vite": "^7.1.1"
},
"dependencies": {
"lodash": "^4.17.21",
"yjs": "^13.6.27"
} }
} }
+65
View File
@@ -1,8 +1,73 @@
import _ from 'lodash';
import './style.css'; import './style.css';
import * as Y from 'yjs';
const app = document.querySelector<HTMLDivElement>('#app')!; const app = document.querySelector<HTMLDivElement>('#app')!;
app.innerHTML = ` app.innerHTML = `
<h1>Story Editor</h1> <h1>Story Editor</h1>
<p>Your TypeScript + Vite app is ready!</p> <p>Your TypeScript + Vite app is ready!</p>
<textarea id="ta1" rows=10 cols=80></textarea>
<br>
<br>
<textarea id="ta2" rows=10 cols=80></textarea>
`; `;
const ta1 = document.getElementById('ta1') as HTMLTextAreaElement;
const ta2 = document.getElementById('ta2') as HTMLTextAreaElement;
function bind(text: Y.Text, el: HTMLTextAreaElement) {
el.value = text.toString();
const handle = _.debounce(() => {
let prev = text.toString();
let curr = el.value;
let i = 0;
for (i = 0; prev[i] && prev[i] == curr[i]; i++) {}
const offset = i;
prev = prev.slice(offset);
curr = curr.slice(offset);
for (
i = 0;
prev[prev.length - 1 - i] &&
prev[prev.length - 1 - i] === curr[curr.length - 1 - i];
i++
) {}
prev = prev.slice(0, prev.length - i);
curr = curr.slice(0, curr.length - i);
text.doc!.transact(() => {
text.delete(offset, prev.length);
text.insert(offset, curr);
});
}, 500);
el.addEventListener('input', (_ev) => {
handle();
});
}
const doc1 = new Y.Doc();
bind(doc1.getText(), ta1);
const doc2 = new Y.Doc();
bind(doc2.getText(), ta2);
doc1.on('update', async (update) => {
await new Promise((res, _rej) =>
setTimeout(res, 3000 * Math.random() + 2000)
);
Y.applyUpdate(doc2, update);
ta2.value = doc2.getText().toString();
});
doc2.on('update', async (update) => {
await new Promise((res, _rej) =>
setTimeout(res, 3000 * Math.random() + 2000)
);
Y.applyUpdate(doc1, update);
ta1.value = doc1.getText().toString();
});
doc1.getText().insert(0, 'foo bar baz');
ta1.value = doc1.getText().toString();
(window as any).Y = Y;