-
Notifications
You must be signed in to change notification settings - Fork 171
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Yjs Electric provider example (#1508)
A codemirror example with Yjs using Electric for sync Contains an Y-Electric connection provider that we will extract and ship as a separate package.
- Loading branch information
Showing
25 changed files
with
1,544 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
|
||
# sst | ||
.sst |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,4 +41,4 @@ export async function GET(request: Request) { | |
}) | ||
} | ||
return resp | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/build/** | ||
sst.config.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
module.exports = { | ||
env: { | ||
browser: true, | ||
es2021: true, | ||
node: true, | ||
}, | ||
extends: [ | ||
`eslint:recommended`, | ||
`plugin:@typescript-eslint/recommended`, | ||
`plugin:prettier/recommended`, | ||
'plugin:@next/next/recommended', | ||
], | ||
parserOptions: { | ||
ecmaVersion: 2022, | ||
requireConfigFile: false, | ||
sourceType: `module`, | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
parser: `@typescript-eslint/parser`, | ||
plugins: [`prettier`], | ||
rules: { | ||
quotes: [`error`, `backtick`], | ||
"no-unused-vars": `off`, | ||
"@typescript-eslint/no-unused-vars": [ | ||
`error`, | ||
{ | ||
argsIgnorePattern: `^_`, | ||
varsIgnorePattern: `^_`, | ||
caughtErrorsIgnorePattern: `^_`, | ||
}, | ||
], | ||
"@next/next/no-img-element": "off" | ||
}, | ||
ignorePatterns: [ | ||
`**/node_modules/**`, | ||
`**/dist/**`, | ||
`tsup.config.ts`, | ||
`vitest.config.ts`, | ||
`.eslintrc.js`, | ||
`**/*.css`, | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
dist | ||
.env.local | ||
|
||
# Turborepo | ||
.turbo | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
next-env.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"trailingComma": "es5", | ||
"semi": false, | ||
"tabWidth": 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
FROM node:lts-alpine AS base | ||
|
||
# Stage 1: Install dependencies | ||
FROM base AS deps | ||
WORKDIR /app | ||
|
||
RUN npm install -g pnpm | ||
|
||
COPY pnpm-*.yaml ./ | ||
COPY package.json ./ | ||
COPY tsconfig.build.json ./ | ||
COPY packages/typescript-client packages/typescript-client/ | ||
COPY packages/react-hooks packages/react-hooks/ | ||
COPY examples/yjs-provider/ examples/yjs-provider/ | ||
|
||
# Install dependencies | ||
RUN pnpm install --frozen-lockfile | ||
RUN pnpm run -r build | ||
|
||
|
||
# Need to make production image more clean | ||
FROM node:lts-alpine AS prod | ||
WORKDIR /app | ||
|
||
ENV NODE_ENV=production | ||
COPY --from=deps /app/ ./ | ||
|
||
WORKDIR /app/examples/yjs-provider/ | ||
|
||
EXPOSE 3000 | ||
CMD ["npm", "run", "start"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Yjs Electric provider | ||
|
||
This example showcases a multiplayer [Codemirror](https://codemirror.net/) editor with [YJS](https://github.com/yjs/yjs) and [ElectricSQL](https://electric-sql.com/). All data is synchronized through [Postgres](https://www.postgresql.org/), eliminating the need for additional real-time infrastructure. | ||
|
||
Y-Electric is a [YJS connection provider](https://docs.yjs.dev/ecosystem/connection-provider) that comes with offline support, integrates with [database providers](https://docs.yjs.dev/ecosystem/database-provider) and also handles [Presence/Awareness](https://docs.yjs.dev/api/about-awareness) data. It works with the entire YJS ecosystem and with you existing apps too! | ||
|
||
> We're releasing The Y-Electric backend as a package soon! | ||
## How to run | ||
|
||
Make sure you've installed all dependencies for the monorepo and built the packages (from the monorepo root directory): | ||
|
||
```shell | ||
pnpm install | ||
pnpm run -r build | ||
``` | ||
|
||
Start the docker containers (in this directory): | ||
|
||
```shell | ||
pnpm backend:up | ||
``` | ||
|
||
Start the dev server: | ||
|
||
```shell | ||
pnpm dev | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Pool } from "pg" | ||
import { NextResponse } from "next/server" | ||
import { neon } from "@neondatabase/serverless" | ||
|
||
// hybrid implementation for connection pool and serverless with neon | ||
|
||
const connectionString = | ||
process.env.NEON_DATABASE_URL || | ||
process.env.DATABASE_URL || | ||
`postgresql://postgres:password@localhost:54321/electric` | ||
|
||
const sql = process.env.NEON_DATABASE_URL ? neon(connectionString) : undefined | ||
|
||
const pool = !process.env.NEON_DATABASE_URL | ||
? new Pool({ connectionString }) | ||
: undefined | ||
|
||
export async function POST(request: Request) { | ||
try { | ||
const { room, op, clientId } = await getRequestParams(request) | ||
if (!clientId) { | ||
await saveOperation(room, op) | ||
} else { | ||
await saveAwarenessOperation(room, op, clientId) | ||
} | ||
return NextResponse.json({}) | ||
} catch (e) { | ||
const resp = e instanceof Error ? e.message : e | ||
return NextResponse.json(resp, { status: 400 }) | ||
} | ||
} | ||
|
||
async function saveOperation(room: string, op: string) { | ||
const q = `INSERT INTO ydoc_operations (room, op) VALUES ($1, decode($2, 'base64'))` | ||
const params = [room, op] | ||
await runQuery(q, params) | ||
} | ||
|
||
async function saveAwarenessOperation( | ||
room: string, | ||
op: string, | ||
clientId: string | ||
) { | ||
const q = `INSERT INTO ydoc_awareness (room, clientId, op) VALUES ($1, $2, decode($3, 'base64')) | ||
ON CONFLICT (clientId, room) | ||
DO UPDATE SET op = decode($3, 'base64'), updated = now()` | ||
const params = [room, clientId, op] | ||
await runQuery(q, params) | ||
} | ||
|
||
async function getRequestParams( | ||
request: Request | ||
): Promise<{ room: string; op: string; clientId?: string }> { | ||
const { room, op, clientId } = await request.json() | ||
if (!room) { | ||
throw new Error(`'room' is required`) | ||
} | ||
if (!op) { | ||
throw new Error(`'op' is required`) | ||
} | ||
|
||
return { room, op, clientId } | ||
} | ||
|
||
async function runQuery(q: string, params: string[]) { | ||
if (pool) { | ||
await pool.query(q, params) | ||
} else if (sql) { | ||
await sql(q, params) | ||
} else { | ||
throw new Error(`No database driver provided`) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
"use client" | ||
|
||
import { useEffect, useRef, useState } from "react" | ||
|
||
import * as Y from "yjs" | ||
import { yCollab, yUndoManagerKeymap } from "y-codemirror.next" | ||
import { ElectricProvider } from "./y-electric" | ||
import { Awareness } from "y-protocols/awareness" | ||
|
||
import { EditorState } from "@codemirror/state" | ||
import { EditorView, basicSetup } from "codemirror" | ||
import { keymap } from "@codemirror/view" | ||
import { javascript } from "@codemirror/lang-javascript" | ||
|
||
import * as random from "lib0/random" | ||
import { IndexeddbPersistence } from "y-indexeddb" | ||
|
||
const room = `electric-demo` | ||
|
||
const usercolors = [ | ||
{ color: `#30bced`, light: `#30bced33` }, | ||
{ color: `#6eeb83`, light: `#6eeb8333` }, | ||
{ color: `#ffbc42`, light: `#ffbc4233` }, | ||
{ color: `#ecd444`, light: `#ecd44433` }, | ||
{ color: `#ee6352`, light: `#ee635233` }, | ||
{ color: `#9ac2c9`, light: `#9ac2c933` }, | ||
] | ||
|
||
const userColor = usercolors[random.uint32() % usercolors.length] | ||
const ydoc = new Y.Doc() | ||
|
||
const isServer = typeof window === `undefined` | ||
|
||
const awareness = !isServer ? new Awareness(ydoc) : undefined | ||
awareness?.setLocalStateField(`user`, { | ||
name: userColor.color, | ||
color: userColor.color, | ||
colorLight: userColor.light, | ||
}) | ||
|
||
const network = !isServer | ||
? new ElectricProvider( | ||
new URL(`/shape-proxy`, window?.location.origin).href, | ||
room, | ||
ydoc, | ||
{ | ||
connect: true, | ||
awareness, | ||
persistence: new IndexeddbPersistence(room, ydoc), | ||
} | ||
) | ||
: undefined | ||
|
||
export default function ElectricEditor() { | ||
const editor = useRef(null) | ||
|
||
const [connectivityStatus, setConnectivityStatus] = useState< | ||
`connected` | `disconnected` | ||
>(`connected`) | ||
|
||
const toggle = () => { | ||
if (!network) { | ||
return | ||
} | ||
const toggleStatus = | ||
connectivityStatus === `connected` ? `disconnected` : `connected` | ||
setConnectivityStatus(toggleStatus) | ||
toggleStatus === `connected` ? network.connect() : network.disconnect() | ||
} | ||
|
||
useEffect(() => { | ||
if (typeof window === `undefined`) { | ||
return | ||
} | ||
|
||
const ytext = ydoc.getText(room) | ||
|
||
const state = EditorState.create({ | ||
doc: ytext.toString(), | ||
extensions: [ | ||
keymap.of([...yUndoManagerKeymap]), | ||
basicSetup, | ||
javascript(), | ||
EditorView.lineWrapping, | ||
yCollab(ytext, awareness), | ||
], | ||
}) | ||
|
||
const view = new EditorView({ state, parent: editor.current ?? undefined }) | ||
|
||
return () => view.destroy() | ||
}) | ||
|
||
return ( | ||
<div> | ||
<form action={async () => toggle()}> | ||
<button type="submit" className="button" name="intent" value="add"> | ||
{connectivityStatus} | ||
</button> | ||
</form> | ||
<p> | ||
This is a demo of <a href="https://github.com/yjs/yjs">Yjs</a> using | ||
{` `} | ||
{` `} | ||
<a href="https://github.com/electric-sql/electric">Electric</a> for | ||
syncing. | ||
</p> | ||
<p> | ||
The content of this editor is shared with every client that visits this | ||
domain. | ||
</p> | ||
<div ref={editor}></div> | ||
</div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export const metadata = { | ||
title: `Yjs <> Electric`, | ||
description: `Yjs synching with Electric`, | ||
} | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode | ||
}) { | ||
return ( | ||
<html lang="en"> | ||
<body>{children}</body> | ||
</html> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"use server" | ||
|
||
import React from "react" | ||
import ElectricEditor from "./electric-editor" | ||
|
||
const Page = async () => <ElectricEditor /> | ||
|
||
export default Page |
Oops, something went wrong.