Skip to content

Commit 262d02c

Browse files
authored
Implement events for sync status (#353)
`y-websocket` has a `synced` flag, but it is a bit misleading because it only tells whether the initial handshake was completed. Knowing whether a document was synced _in general_ for `y-protocols` is a bit tricky, because we would need to compare the state vector and delete set. When using WebSockets, we can take advantage of the ordering guarantee and synchronous message processing in Y-Sweet to implement a very simple check of sync. The client stores two numbers, `lastSyncSent` and `lastSyncAcked`. When it sends an update to the server, it also increments `lastSyncSent` and then sends a separate `messageSyncStatus` message containing that number as the payload. When the server receives a `messageSyncStatus`, it simply echoes it verbatim. When the client receives the `messageSyncStatus` in return, it updates `lastSyncAcked` to the payload of the message. If `lastSyncAcked` = `lastSyncSent`, we know that all messages since the last update have been processed. Otherwise, there are outstanding changes. This builds on #352 and will enable #306. Demo: https://github.com/user-attachments/assets/60d4aec2-f025-4e84-a594-28fbc84c67cb
1 parent a881ae7 commit 262d02c

10 files changed

Lines changed: 402 additions & 166 deletions

File tree

crates/y-sweet/src/server.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use futures::{SinkExt, StreamExt};
1717
use serde::Deserialize;
1818
use serde_json::{json, Value};
1919
use std::{
20-
net::SocketAddr,
2120
sync::{Arc, RwLock},
2221
time::Duration,
2322
};

examples/nextjs/src/app/(demos)/color-grid/ColorGrid.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function ColorGrid() {
1414
const [color, setColor] = useState<string | null>(COLORS[0])
1515

1616
return (
17-
<div className="space-y-3 p-4 lg:p-8">
17+
<div className="space-y-3">
1818
<Title>Color Grid</Title>
1919
<div className="space-x-2 flex flex-row">
2020
{COLORS.map((c) => (
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { YDocProvider } from '@y-sweet/react'
22
import { randomId } from '@/lib/utils'
33
import { ColorGrid } from './ColorGrid'
4+
import StateIndicator from '@/components/StateIndicator'
45

56
export default function Home({ searchParams }: { searchParams: { doc: string } }) {
67
const docId = searchParams.doc ?? randomId()
78
return (
89
<YDocProvider docId={docId} setQueryParam="doc" authEndpoint="/api/auth">
9-
<ColorGrid />
10+
<div className="p-4 lg:p-8">
11+
<StateIndicator />
12+
<ColorGrid />
13+
</div>
1014
</YDocProvider>
1115
)
1216
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client'
2+
3+
import { STATUS_CONNECTED } from '@y-sweet/client'
4+
import { useConnectionStatus, useHasLocalChanges } from '@y-sweet/react'
5+
6+
export default function StateIndicator() {
7+
let connectionStatus = useConnectionStatus()
8+
let hasLocalChanges = useHasLocalChanges()
9+
10+
let statusColor = connectionStatus === STATUS_CONNECTED ? 'bg-green-500' : 'bg-red-500'
11+
let syncedColor = hasLocalChanges ? 'bg-red-500' : 'bg-green-500'
12+
13+
return (
14+
<div className="mb-4">
15+
<div className="flex flex-row items-center text-xs space-x-1 w-fit bg-white rounded-md p-1 text-gray-500">
16+
<div>CONNECTED:</div>
17+
<div
18+
className={`w-3 h-3 rounded-full transition-colors ${statusColor}`}
19+
title={connectionStatus}
20+
></div>
21+
<div>SYNCED:</div>
22+
<div
23+
className={`w-3 h-3 rounded-full transition-colors ${syncedColor}`}
24+
title={hasLocalChanges ? 'Unsynced local changes.' : 'No unsynced local changes.'}
25+
></div>
26+
</div>
27+
</div>
28+
)
29+
}

js-pkg/client/src/main.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
1-
import { YSweetProvider, type YSweetProviderParams, type AuthEndpoint } from './provider'
2-
import * as Y from 'yjs'
31
import { ClientToken, encodeClientToken } from '@y-sweet/sdk'
4-
export { YSweetProvider, YSweetProviderParams, AuthEndpoint }
2+
import * as Y from 'yjs'
3+
import {
4+
type AuthEndpoint,
5+
EVENT_CONNECTION_STATUS,
6+
EVENT_LOCAL_CHANGES,
7+
STATUS_CONNECTED,
8+
STATUS_CONNECTING,
9+
STATUS_ERROR,
10+
STATUS_HANDSHAKING,
11+
STATUS_OFFLINE,
12+
YSweetProvider,
13+
type YSweetProviderParams,
14+
type YSweetStatus,
15+
} from './provider'
16+
export {
17+
AuthEndpoint,
18+
EVENT_CONNECTION_STATUS,
19+
EVENT_LOCAL_CHANGES,
20+
STATUS_CONNECTED,
21+
STATUS_CONNECTING,
22+
STATUS_ERROR,
23+
STATUS_HANDSHAKING,
24+
STATUS_OFFLINE,
25+
YSweetProvider,
26+
YSweetProviderParams,
27+
YSweetStatus,
28+
}
529

630
/**
731
* Given a docId and {@link AuthEndpoint}, create a {@link YSweetProvider} for it.

0 commit comments

Comments
 (0)