Your README.md file should have:
- Detailed instructions to run your site.
- The URL to where you are hosting the site on web.
This project explores the use of Conflict-free Replicated Data Types (CRDTs) and the Yjs library to create a collaborative, real-time application inspired by the great brush/ink artists of Chinese and Japanese traditional brush painting.
This guide will walk you through the steps to visit Brush and Poem and sync your changes between different app instances. This can be useful if you want to view the same content on multiple devices or browser tabs.
Open your web browser and visit https://brushandpoem.netlify.app/.
In the first input enter a username. This is stored locally and is NOT secure. It just keeps a record of your poems.
Scroll down to the list menu at the bottom of the page. Here, you will see a list of artists. Click on one of the artists to view their content.
Now, open a new tab in your browser or a new browser on a different device. Visit https://brushandpoem.netlify.app/ again.
In the new tab or device, scroll down to the list menu at the bottom of the page. Click on the same poet that you selected in the first tab or device.
Now, any changes you make in one tab or device (like selecting a different poet or viewing different content) will be synced to the other. This allows you to view the same content across multiple devices or browser tabs.
Remember, you should have a working internet connection. The content, however, should save locally. This is one of the principles of going "Local First".
CRDTs are data structures that allow many replicas at the same time to be updated independently, taking care of merging changes (a lot like Github).
In this project, Yjs is used to allow multiple users to collaboratively create and edit visual representations and text of original poems. Users can work on the same poem simultaneously, with all their changes automatically and deterministically merged. D3 actively visualizes the poems and Compromise actively analyzes the text.
The visual design of the application is inspired by some of the great brush/ink artists of Chinese and Japanese traditional brush painting. The application provides a canvas where users can create visual representations of poems, where poems are turned into graphs.
To run the project locally, use the following command:
git clone https://github.com/cscie114/csci-e-114-final-project-cre8ture
Then go into the main root folder
npm install
Then run the app locally
npm start
If you have Netlify Dev CLI installed, you can run the app within a netlify environment (great for testing the edge functions):
$env:GATSBY_EXPERIMENTAL_DEV_SSR="true"; gatsby develop
You can then test the project by sending a POST request to the local server:
curl.exe -X POST "http://localhost:8888/.netlify/functions/retrieveOffer" -H "Content-Type: application/json" -d '{\"peerId\": \"your-peer-id\"}'
Conflict-free Replicated Data Types (CRDTs) are data structures that enable multiple replicas to be updated independently without coordination. The updates can then be merged deterministically, ensuring strong eventual consistency across all replicas.
Yjs is a JavaScript library that implements CRDTs. These allow developers to build collaborative applications where users can work together in real-time.
In my explorations of CDRTs I was curious to understand if I could avoid a backend server entirely. Ultimately, I wound up using the public YJS demo servr (not for production).
My original plan was to use WebRTC and create a number of manual IDs or peers and each time a user opens an instance of the app, the app randomly chooses an ID and then randomly at intervals searches for another peer nearby (using one of the 10 or so IDs stored). Ulimtinatley, this solution was not performant (too many searches.)
Yjs provides several data types, including Y.Text, Y.Array, Y.Map, and others. These data types can be updated independently on different replicas, and the changes will be merged automatically.
Here's an example of how to create a Yjs document and a Y.Text type:
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const ytext = ydoc.getText('text');
In this code, new Y.Doc()
creates a new Yjs document, and ydoc.getText('text')
creates a new Y.Text type in the document.
Yjs supports various network providers for syncing changes between different replicas. One of them is the WebsocketProvider, which uses WebSockets for real-time communication.
Here's an example of how to set up a WebsocketProvider:
import { WebsocketProvider } from 'y-websocket';
const provider = new WebsocketProvider(
'wss://demos.yjs.dev/ws',
'my-room',
ydoc
);
In this code, new WebsocketProvider('wss://demos.yjs.dev/ws', 'my-room', ydoc)
creates a new WebsocketProvider that connects to the WebSocket server at 'wss://demos.yjs.dev/ws' and syncs changes for the 'my-room' room.
Yjs data types emit events when they are changed. You can observe these events to update your application state.
Here's an example of how to observe changes to a Y.Text type:
ytext.observe(event => {
console.log('Text changed:', ytext.toString());
});
In this code, ytext.observe(event => {...})
sets up an observer that logs the new text whenever the Y.Text type is changed.
You can update Yjs data types using their methods. For example, you can use the insert
and delete
methods to update a Y.Text type:
ytext.insert(0, 'Hello, world!');
ytext.delete(0, 1);
In this code, ytext.insert(0, 'Hello, world!')
inserts 'Hello, world!' at the beginning of the text, and ytext.delete(0, 1)
deletes the first character.
In the gatsby-node.js
file, the onCreateWebpackConfig
function is used to extend the default Webpack configuration used by Gatsby. This is done using the setWebpackConfig
action provided by Gatsby.
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true
},
module: {
rules: [
{
test: /\.wasm$/,
type: "webassembly/async",
},
],
},
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
});
};
In this code, the setWebpackConfig
action is used to enable WebAssembly experiments, add a rule for .wasm
files, and set performance hints. This was mainly due to my attenpt to run AutoMerge - which uses WASM under the hood. I struggled to get it to run (choosing YJS ultimately).
The createPages
function is used to fetch data from the Wikipedia API for a list of topics, and create a page for each topic.
exports.createPages = async ({ actions }) => {
const { createPage } = actions;
const fetch = (await import('node-fetch')).default;
const topics = ['Xu_Wei', 'Tosa_Mitsuoki', 'Ike_no_Taiga', 'Yosa_Buson'];
const pagesData = await Promise.all(topics.map(async topic => {
const response = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${topic}`);
const data = await response.json();
const imageUrl = data.thumbnail ? data.thumbnail.source : null;
return {
title: data.title,
summary: data.extract,
imageUrl: imageUrl,
author: 'Wikipedia'
};
}));
pagesData.forEach(data => {
createPage({
path: `/study-guide/${data.title}`,
component: path.resolve(`./src/templates/studyGuide.js`),
context: {
title: data.title,
summary: data.summary,
imageUrl: data.imageUrl,
author: data.author,
},
});
});
};
In this code, the createPages
function fetches data from the Wikipedia API for each topic, and then uses the createPage
action to create a page for each topic. The fetched data is passed to the page component via the context
property.
While this gatsby-node.js
file does not directly use GraphQL, Gatsby uses GraphQL extensively for data querying. The data passed to the page component via the context
property can be queried using GraphQL in the page component. For example, you could use a GraphQL query like the following in your studyGuide.js
template to access the data:
export const query = graphql`
query($title: String!) {
markdownRemark(frontmatter: { title: { eq: $title } }) {
frontmatter {
title
summary
imageUrl
author
}
}
}
`;
In this query, $title
is a variable that gets its value from the context
property passed in createPage
. The query fetches the title, summary, image URL, and author of the markdown file with the matching title.