2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Yjs - S2 Cloudflare Worker
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | ## Overview
30 |
31 | Y-S2 is a Cloudflare Worker that provides real-time collaborative document editing using Yjs with S2.dev as the distribution channel and an R2 bucket as the storage provider. It provides scalable WebSocket-based document synchronization where document updates are made durable on an S2 stream and distributed to connected clients and reactively persisted to the R2 bucket.
32 |
33 | ## Getting Started
34 |
35 | ### Prerequisites
36 |
37 | - Cloudflare account with Workers enabled.
38 | - S2.dev account, a basin with `Create stream on append/read` enabled, and a scoped access token to the basin you want to use.
39 | - R2 bucket for snapshot storage.
40 |
41 | ### Environment Variables
42 |
43 | ```bash
44 | S2_ACCESS_TOKEN=your_s2_access_token
45 | S2_BASIN=your_s2_basin_name
46 | R2_BUCKET=your_r2_bucket_name
47 | LOG_MODE=CONSOLE|S2_SINGLE|S2_SHARED # Optional
48 | SNAPSHOT_BACKLOG_SIZE=100 # Optional
49 | ```
50 |
51 | ### Deployment
52 |
53 | ```bash
54 | # Install dependencies
55 | npm install
56 |
57 | # Deploy to Cloudflare Workers
58 | npm run deploy
59 |
60 | # Or run locally for development
61 | npm run dev
62 | ```
63 |
64 | ### Client Integration
65 |
66 | Connect to the deployed worker using the Yjs WebSocket provider:
67 |
68 | ```javascript
69 | import * as Y from 'yjs'
70 | import { WebsocketProvider } from 'y-websocket'
71 |
72 | const doc = new Y.Doc()
73 | const provider = new WebsocketProvider('wss://your-worker.your-subdomain.workers.dev', 'room-name', doc, {
74 | params: { authToken: 'your-auth-token' }
75 | })
76 |
77 | ```
78 |
79 | ### Credits
80 |
81 | Portions of this project are derived from [y-redis](https://github.com/yjs/y-redis), licensed under the GNU Affero General Public License v3.0.
82 |
--------------------------------------------------------------------------------
/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s2-streamstore/y-s2/9241675e16b01cc4f49ec1da92bd79562b4faa9b/assets/demo.gif
--------------------------------------------------------------------------------
/assets/s2-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s2-streamstore/y-s2/9241675e16b01cc4f49ec1da92bd79562b4faa9b/assets/s2-black.png
--------------------------------------------------------------------------------
/assets/s2-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/s2-streamstore/y-s2/9241675e16b01cc4f49ec1da92bd79562b4faa9b/assets/s2-white.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "y-s2",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "deploy": "wrangler deploy",
7 | "dev": "wrangler dev",
8 | "start": "wrangler dev",
9 | "test": "vitest",
10 | "cf-typegen": "wrangler types",
11 | "format": "prettier --write \"src/**/*.{ts,js,json}\"",
12 | "format:check": "prettier --check \"src/**/*.{ts,js,json}\"",
13 | "lint": "eslint src --ext .ts,.js",
14 | "lint:fix": "eslint src --ext .ts,.js --fix"
15 | },
16 | "devDependencies": {
17 | "@cloudflare/vitest-pool-workers": "^0.8.19",
18 | "@types/node": "^24.3.0",
19 | "@typescript-eslint/eslint-plugin": "^6.0.0",
20 | "@typescript-eslint/parser": "^6.0.0",
21 | "eslint": "^8.0.0",
22 | "install": "^0.13.0",
23 | "npm": "^11.5.2",
24 | "prettier": "^3.0.0",
25 | "typescript": "^5.5.2",
26 | "vitest": "~3.2.0",
27 | "wrangler": "^4.33.1"
28 | },
29 | "dependencies": {
30 | "@s2-dev/streamstore": "^0.15.9",
31 | "js-base64": "^3.7.8",
32 | "lib0": "^0.2.114",
33 | "y-protocols": "^1.0.5",
34 | "yjs": "^13.6.27"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { S2 } from '@s2-dev/streamstore';
2 | import type { EventStream } from '@s2-dev/streamstore/lib/event-streams.js';
3 | import { S2Format, type ReadEvent } from '@s2-dev/streamstore/models/components';
4 | import { TailResponse } from '@s2-dev/streamstore/models/errors';
5 | import * as Y from 'yjs';
6 | import * as decoding from 'lib0/decoding';
7 | import * as awarenessProtocol from 'y-protocols/awareness';
8 | import * as array from 'lib0/array';
9 | import { toUint8Array } from 'js-base64';
10 | import { createLogger, S2Logger } from './logger.js';
11 | import {
12 | encodeAwarenessUpdate,
13 | encodeAwarenessUserDisconnected,
14 | encodeSyncStep1,
15 | encodeSyncStep2,
16 | messageAwareness,
17 | messageSync,
18 | messageSyncStep1,
19 | messageSyncStep2,
20 | messageSyncUpdate,
21 | } from './protocol.js';
22 | import { retrieveSnapshot, uploadSnapshot, getSnapshotETag } from './snapshot.js';
23 | import {
24 | decodeBigEndian64AsNumber,
25 | generateDeadlineFencingToken,
26 | isFenceCommand,
27 | isTrimCommand,
28 | MessageBatcher,
29 | parseConfig,
30 | parseFencingToken,
31 | Room,
32 | } from './utils.js';
33 | import { createSnapshotState, createUserState, SnapshotState } from './types.js';
34 |
35 | export interface Env {
36 | // S2 access token
37 | S2_ACCESS_TOKEN: string;
38 | // S2 basin name
39 | S2_BASIN: string;
40 | // R2 bucket for snapshots
41 | R2_BUCKET: R2Bucket;
42 | // Logging mode: CONSOLE | S2_SINGLE | S2_SHARED
43 | // CONSOLE: logs to console only
44 | // S2_SINGLE: logs to a single S2 stream with a unique worker ID
45 | // S2_SHARED: logs to a shared S2 stream with a worker ID
46 | LOG_MODE?: string;
47 | // Size of the record backlog to trigger a snapshot
48 | SNAPSHOT_BACKLOG_SIZE?: string;
49 | // Maximum age of a collected snapshot buffer before it is persisted to R2
50 | BACKLOG_BUFFER_AGE?: number;
51 | // Maximum batch size to reach before flushing to S2
52 | S2_BATCH_SIZE?: string;
53 | // Maximum time to wait before flushing a batch to S2
54 | S2_LINGER_TIME?: string;
55 | // Fencing token lease duration in seconds
56 | // Represented as: `{id} {leaseDeadline}`
57 | LEASE_DURATION?: number;
58 | }
59 |
60 | export default {
61 | async fetch(request: Request, env: Env): Promise