Skip to content

Commit

Permalink
add onPendingChangesUpdated
Browse files Browse the repository at this point in the history
  • Loading branch information
nikgraf committed Jun 3, 2024
1 parent 0e19656 commit cdce17c
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import sodium, { KeyPair } from "libsodium-wrappers";
import React, { useRef, useState } from "react";
import { DevTool } from "secsync-react-devtool";
import { useYjsSync } from "secsync-react-yjs";
import * as Yjs from "yjs";
import { useYArray } from "../../hooks/useYArray";

const websocketEndpoint =
process.env.NODE_ENV === "development"
? "ws://localhost:4000"
: "wss://secsync.fly.dev";

type Props = {
documentId: string;
showDevTool: boolean;
};

export const YjsPendingChangesCallbackExample: React.FC<Props> = ({
documentId,
showDevTool,
}) => {
const documentKey = sodium.from_base64(
"MTcyipWZ6Kiibd5fATw55i9wyEU7KbdDoTE_MRgDR98"
);

const [authorKeyPair] = useState<KeyPair>(() => {
return sodium.crypto_sign_keypair();
});

const yDocRef = useRef<Yjs.Doc>(new Yjs.Doc());
const yTodos: Yjs.Array<string> = yDocRef.current.getArray("todos");
const todos = useYArray(yTodos);
const [newTodoText, setNewTodoText] = useState("");

const [state, send] = useYjsSync({
onPendingChangesUpdated: (allChanges) => {
console.log("pending changes", allChanges);
},
yDoc: yDocRef.current,
documentId,
signatureKeyPair: authorKeyPair,
websocketEndpoint,
websocketSessionKey: "your-secret-session-key",
getNewSnapshotData: async ({ id }) => {
return {
data: Yjs.encodeStateAsUpdateV2(yDocRef.current),
key: documentKey,
publicData: {},
};
},
getSnapshotKey: async () => {
return documentKey;
},
shouldSendSnapshot: ({ snapshotUpdatesCount }) => {
// create a new snapshot if the active snapshot has more than 100 updates
return snapshotUpdatesCount > 3;
},
isValidClient: async (signingPublicKey: string) => {
return true;
},
sodium,
logging: "error",
});

return (
<>
<div className="todoapp">
<form
onSubmit={(event) => {
event.preventDefault();
yTodos.push([newTodoText]);
setNewTodoText("");
}}
>
<input
placeholder="What needs to be done?"
onChange={(event) => setNewTodoText(event.target.value)}
value={newTodoText}
className="new-todo"
/>
<button className="add">Add</button>
</form>

<ul className="todo-list">
{todos.map((entry, index) => {
return (
<li key={`${index}-${entry}`}>
<div className="edit">{entry}</div>
<button
className="destroy"
onClick={() => {
yTodos.delete(index, 1);
}}
/>
</li>
);
})}
</ul>
</div>

<div className="mt-8" />
<DevTool state={state} send={send} />
</>
);
};
1 change: 1 addition & 0 deletions documentation/pages/docs/_meta.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"getting-started": "Getting started",
"integration-examples": "Integration Examples",
"other-examples": "Other Examples",
"api": "API",
"error-handling": "Error Handling",
"security_and_privacy": "Security & Privacy",
Expand Down
10 changes: 10 additions & 0 deletions documentation/pages/docs/api/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ The `knownSnapshotInfo` can be stored locally in order to provide it to `loadDoc
}) => void | Promise<void>
```

#### onPendingChangesUpdated

Required: `false`

A callback that is invoked every time the pending changes of a document are updated. It combines all unsynced changes, changes from a snapshot in flight and changes from updates in flight.

```ts
(pendingChanges: any[]) => void | Promise<void>
```

#### loadDocumentParams

Required: `false`
Expand Down
17 changes: 17 additions & 0 deletions documentation/pages/docs/other-examples/pending-changes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Pending Changes Example

This example shows how to use the `onPendingChangesUpdated` callback to get the unsynced changes. This is useful to update a local storage to avoid changes getting lost e.g. in a local-first setting.

See the `console.log` output in the browser console to see the pending changes.

import SimpleExampleWrapper from "../../../components/SimpleExampleWrapper";
import { YjsPendingChangesCallbackExample } from "../../../components/YjsPendingChangesCallbackExample/YjsPendingChangesCallbackExample";

<SimpleExampleWrapper
component={YjsPendingChangesCallbackExample}
generateDocumentKey={false}
/>

## Code

The code for this example is located [here](https://github.com/serenity-kit/secsync/blob/main/documentation/components/YjsPendingChangesCallbackExample/YjsPendingChangesCallbackExample.tsx).
92 changes: 62 additions & 30 deletions packages/secsync/src/createSyncMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,17 @@ export const createSyncMachine = () => {
};
},
actions: {
invokePendingChangesUpdatedCallback: ({ context, event }) => {
console.log("event", event);
const allPendingChanges = [
...context._pendingChangesQueue,
...(context._snapshotInFlight?.changes || []),
...context._updatesInFlight.reduce((accumulator, updateInFlight) => {
return [...accumulator, ...updateInFlight.changes];
}, [] as any[]),
];
context.onPendingChangesUpdated?.(allPendingChanges);
},
resetWebsocketRetries: assign({
_websocketRetries: 0,
}),
Expand Down Expand Up @@ -1293,10 +1304,10 @@ export const createSyncMachine = () => {
if (castedEvent.output.handledQueue === "incoming") {
return {
_incomingQueue: context._incomingQueue.slice(1),
// because changes might have been add while processing the queue ans creating a
// snapshot or update we can't just overwrite the _pendingChangesQueue
// instead we need to track how many to remove from the beginning of the list
// and of some should be restored also add them to the list
// Because changes might have been added while processing the queue and therefor
// creating a snapshot or update we can't just overwrite the _pendingChangesQueue.
// Instead we need to track how many to remove from the beginning of the list
// and of some should be restored also add them to the list.
_pendingChangesQueue:
castedEvent.output.pendingChangesToPrepend.concat(
context._pendingChangesQueue.slice(
Expand All @@ -1321,10 +1332,10 @@ export const createSyncMachine = () => {
} else if (castedEvent.output.handledQueue === "customMessage") {
return {
_customMessageQueue: context._customMessageQueue.slice(1),
// because changes might have been add while processing the queue ans creating a
// snapshot or update we can't just overwrite the _pendingChangesQueue
// instead we need to track how many to remove from the beginning of the list
// and of some should be restored also add them to the list
// Because changes might have been added while processing the queue and therefor
// creating a snapshot or update we can't just overwrite the _pendingChangesQueue.
// Instead we need to track how many to remove from the beginning of the list
// and of some should be restored also add them to the list.
_pendingChangesQueue:
castedEvent.output.pendingChangesToPrepend.concat(
context._pendingChangesQueue.slice(
Expand All @@ -1348,10 +1359,10 @@ export const createSyncMachine = () => {
};
} else if (castedEvent.output.handledQueue === "pending") {
return {
// because changes might have been add while processing the queue ans creating a
// snapshot or update we can't just overwrite the _pendingChangesQueue
// instead we need to track how many to remove from the beginning of the list
// and of some should be restored also add them to the list
// Because changes might have been added while processing the queue and therefor
// creating a snapshot or update we can't just overwrite the _pendingChangesQueue.
// Instead we need to track how many to remove from the beginning of the list
// and of some should be restored also add them to the list.
_pendingChangesQueue:
castedEvent.output.pendingChangesToPrepend.concat(
context._pendingChangesQueue.slice(
Expand Down Expand Up @@ -1510,7 +1521,10 @@ export const createSyncMachine = () => {
target: "connected",
},
ADD_CHANGES: {
actions: ["addToPendingUpdatesQueue"],
actions: [
"addToPendingUpdatesQueue",
"invokePendingChangesUpdatedCallback",
],
},
},
},
Expand All @@ -1528,7 +1542,10 @@ export const createSyncMachine = () => {
target: "processingQueues",
},
ADD_CHANGES: {
actions: ["addToPendingUpdatesQueue"],
actions: [
"addToPendingUpdatesQueue",
"invokePendingChangesUpdatedCallback",
],
target: "processingQueues",
},
},
Expand All @@ -1542,7 +1559,10 @@ export const createSyncMachine = () => {
actions: ["addToCustomMessageQueue"],
},
ADD_CHANGES: {
actions: ["addToPendingUpdatesQueue"],
actions: [
"addToPendingUpdatesQueue",
"invokePendingChangesUpdatedCallback",
],
},
},
invoke: {
Expand Down Expand Up @@ -1574,27 +1594,36 @@ export const createSyncMachine = () => {
)
);
},
actions: ["removeOldestItemFromQueueAndUpdateContext"],
actions: [
"removeOldestItemFromQueueAndUpdateContext",
"invokePendingChangesUpdatedCallback",
],
target: "checkingForMoreQueueItems",
},
{
actions: ["removeOldestItemFromQueueAndUpdateContext"],
actions: [
"removeOldestItemFromQueueAndUpdateContext",
"invokePendingChangesUpdatedCallback",
],
target: "#syncMachine.disconnected",
},
],
onError: {
actions: assign(({ context, event }) => {
return {
_documentDecryptionState:
// @ts-expect-error documentDecryptionState is dynamically added to the error event
event.error?.documentDecryptionState ||
context._documentDecryptionState,
_snapshotAndUpdateErrors: [
event.error as Error,
...context._snapshotAndUpdateErrors,
],
};
}),
actions: [
assign(({ context, event }) => {
return {
_documentDecryptionState:
// @ts-expect-error documentDecryptionState is dynamically added to the error event
event.error?.documentDecryptionState ||
context._documentDecryptionState,
_snapshotAndUpdateErrors: [
event.error as Error,
...context._snapshotAndUpdateErrors,
],
};
}),
"invokePendingChangesUpdatedCallback",
],
target: "#syncMachine.failed",
},
},
Expand Down Expand Up @@ -1645,7 +1674,10 @@ export const createSyncMachine = () => {
},
on: {
ADD_CHANGES: {
actions: ["addToPendingUpdatesQueue"],
actions: [
"addToPendingUpdatesQueue",
"invokePendingChangesUpdatedCallback",
],
},
CONNECT: {
target: "connecting",
Expand Down
1 change: 1 addition & 0 deletions packages/secsync/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export type SyncMachineConfig = {
type: OnDocumentUpdatedEventType;
knownSnapshotInfo: SnapshotInfoWithUpdateClocks;
}) => void | Promise<void>;
onPendingChangesUpdated?: (allChanges: any[]) => void;
onCustomMessage?: (message: any) => Promise<void> | void;
loadDocumentParams?: LoadDocumentParams;
additionalAuthenticationDataValidations?: AdditionalAuthenticationDataValidations;
Expand Down

0 comments on commit cdce17c

Please sign in to comment.