Skip to content

Commit

Permalink
Implement events for sync status (#353)
Browse files Browse the repository at this point in the history
`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
  • Loading branch information
paulgb authored Dec 12, 2024
1 parent a881ae7 commit 262d02c
Show file tree
Hide file tree
Showing 10 changed files with 402 additions and 166 deletions.
1 change: 0 additions & 1 deletion crates/y-sweet/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use futures::{SinkExt, StreamExt};
use serde::Deserialize;
use serde_json::{json, Value};
use std::{
net::SocketAddr,
sync::{Arc, RwLock},
time::Duration,
};
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs/src/app/(demos)/color-grid/ColorGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function ColorGrid() {
const [color, setColor] = useState<string | null>(COLORS[0])

return (
<div className="space-y-3 p-4 lg:p-8">
<div className="space-y-3">
<Title>Color Grid</Title>
<div className="space-x-2 flex flex-row">
{COLORS.map((c) => (
Expand Down
6 changes: 5 additions & 1 deletion examples/nextjs/src/app/(demos)/color-grid/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { YDocProvider } from '@y-sweet/react'
import { randomId } from '@/lib/utils'
import { ColorGrid } from './ColorGrid'
import StateIndicator from '@/components/StateIndicator'

export default function Home({ searchParams }: { searchParams: { doc: string } }) {
const docId = searchParams.doc ?? randomId()
return (
<YDocProvider docId={docId} setQueryParam="doc" authEndpoint="/api/auth">
<ColorGrid />
<div className="p-4 lg:p-8">
<StateIndicator />
<ColorGrid />
</div>
</YDocProvider>
)
}
29 changes: 29 additions & 0 deletions examples/nextjs/src/components/StateIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client'

import { STATUS_CONNECTED } from '@y-sweet/client'
import { useConnectionStatus, useHasLocalChanges } from '@y-sweet/react'

export default function StateIndicator() {
let connectionStatus = useConnectionStatus()
let hasLocalChanges = useHasLocalChanges()

let statusColor = connectionStatus === STATUS_CONNECTED ? 'bg-green-500' : 'bg-red-500'
let syncedColor = hasLocalChanges ? 'bg-red-500' : 'bg-green-500'

return (
<div className="mb-4">
<div className="flex flex-row items-center text-xs space-x-1 w-fit bg-white rounded-md p-1 text-gray-500">
<div>CONNECTED:</div>
<div
className={`w-3 h-3 rounded-full transition-colors ${statusColor}`}
title={connectionStatus}
></div>
<div>SYNCED:</div>
<div
className={`w-3 h-3 rounded-full transition-colors ${syncedColor}`}
title={hasLocalChanges ? 'Unsynced local changes.' : 'No unsynced local changes.'}
></div>
</div>
</div>
)
}
30 changes: 27 additions & 3 deletions js-pkg/client/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import { YSweetProvider, type YSweetProviderParams, type AuthEndpoint } from './provider'
import * as Y from 'yjs'
import { ClientToken, encodeClientToken } from '@y-sweet/sdk'
export { YSweetProvider, YSweetProviderParams, AuthEndpoint }
import * as Y from 'yjs'
import {
type AuthEndpoint,
EVENT_CONNECTION_STATUS,
EVENT_LOCAL_CHANGES,
STATUS_CONNECTED,
STATUS_CONNECTING,
STATUS_ERROR,
STATUS_HANDSHAKING,
STATUS_OFFLINE,
YSweetProvider,
type YSweetProviderParams,
type YSweetStatus,
} from './provider'
export {
AuthEndpoint,
EVENT_CONNECTION_STATUS,
EVENT_LOCAL_CHANGES,
STATUS_CONNECTED,
STATUS_CONNECTING,
STATUS_ERROR,
STATUS_HANDSHAKING,
STATUS_OFFLINE,
YSweetProvider,
YSweetProviderParams,
YSweetStatus,
}

/**
* Given a docId and {@link AuthEndpoint}, create a {@link YSweetProvider} for it.
Expand Down
Loading

0 comments on commit 262d02c

Please sign in to comment.