Skip to content

Commit

Permalink
Add validator concept to reflect-yjs!
Browse files Browse the repository at this point in the history
  • Loading branch information
aboodman committed Dec 13, 2023
1 parent 6b5e9db commit 4ccaacc
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 43 deletions.
7 changes: 4 additions & 3 deletions examples/monaco/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
"devDependencies": {
"@rocicorp/eslint-config": "^0.5.1",
"@rocicorp/prettier-config": "^0.2.0",
"@rocicorp/reflect-yjs": "0.0.1",
"@rocicorp/reflect": "^0.38.202311200859",
"@types/react-dom": "^18.2.17",
"@rocicorp/reflect-yjs": "0.0.1",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.0",
"badwords-list": "^1.0.0",
"concurrently": "^8.2.2",
"monaco-editor": "^0.44.0",
"nanoid": "^5.0.3",
"react-dom": "^18.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.3.2",
"vite": "^5.0.2",
"y-monaco": "^0.1.5"
Expand Down
1 change: 0 additions & 1 deletion examples/monaco/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const reflect = new Reflect({
server,
userID,
roomID,
auth: userID,
mutators,
});

Expand Down
40 changes: 18 additions & 22 deletions examples/monaco/src/reflect/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import type {AuthHandler, ReflectServerOptions} from '@rocicorp/reflect/server';
import {mutators as yjsMutators, Mutators} from '@rocicorp/reflect-yjs';
import {mutators as yjsMutators, updateYJS} from '@rocicorp/reflect-yjs';
import {regex} from 'badwords-list';

const authHandler: AuthHandler = (auth: string, _roomID: string) => {
if (auth) {
// A real implementation could:
// 1. if using session auth make a fetch call to a service to
// look up the userID by `auth` in a session database.
// 2. if using stateless JSON Web Token auth, decrypt and validate the token
// and return the sub field value for userID (i.e. subject field).
// It should also check that the user with userID is authorized
// to access the room with roomID.
return {
userID: auth,
};
}
return null;
};

function makeOptions(): ReflectServerOptions<Mutators> {
function makeOptions() {
return {
mutators: yjsMutators,
authHandler,
logLevel: 'debug',
mutators: {
...yjsMutators,
updateYJS: updateYJS({
validator: doc => {
const text = doc.getText('monaco');
const string = text.toString();
let match: RegExpExecArray | null = null;
while ((match = regex.exec(string)) !== null) {
const badWordLength = match[0].length;
text.delete(match.index, badWordLength);
text.insert(match.index, '*'.repeat(badWordLength));
}
},
}),
},
};
}

Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/reflect-yjs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export {Awareness} from './awareness.js';
export {mutators, type Mutators} from './mutators.js';
export {mutators, type Mutators, updateYJS} from './mutators.js';
export {Provider} from './provider.js';
51 changes: 36 additions & 15 deletions packages/reflect-yjs/src/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,48 @@ import {chunk, unchunk} from './chunk.js';
export const mutators = {
yjsSetLocalStateField,
yjsSetLocalState,
updateYJS,
updateYJS: updateYJS(undefined),
};

export type Mutators = typeof mutators;

export async function updateYJS(
tx: WriteTransaction,
{name, update}: {name: string; update: string},
) {
if (tx.location === 'server') {
const existingServerUpdate = await getServerUpdate(name, tx);
if (!existingServerUpdate) {
await setServerUpdate(name, base64.toByteArray(update), tx);
} else {
const updates = [existingServerUpdate, base64.toByteArray(update)];
const merged = Y.mergeUpdatesV2(updates);
export type UpdateYJSArgs = {
validator?: ((doc: Y.Doc) => void) | undefined;
};

export function updateYJS(args?: UpdateYJSArgs | undefined) {
return async function (
tx: WriteTransaction,
{name, update}: {name: string; update: string},
) {
const {validator} = args ?? {};
if (tx.location === 'server') {
const existingServerUpdate = await getServerUpdate(name, tx);
const decodedUpdate = base64.toByteArray(update);
let merged = existingServerUpdate
? Y.mergeUpdatesV2([existingServerUpdate, decodedUpdate])
: decodedUpdate;

if (validator) {
// If we have a validator, we need to materialize the doc.
// This is slow, but we'll add features to Reflect in the future to keep this doc
// loaded so we don't have to do it over and over. Currently we cannot because it is
// possible for multiple rooms to be loaded into the same JS context, so global
// variables don't work. We need some shared context that we can stash cross-mutator
// state like this on.
const doc = new Y.Doc();
Y.applyUpdateV2(doc, merged);
validator(doc);
merged = Y.encodeStateAsUpdateV2(doc);
}
await setServerUpdate(name, merged, tx);
} else {
if (validator) {
throw new Error('validator only supported on server');
}
await setClientUpdate(name, update, tx);
}
} else {
await setClientUpdate(name, update, tx);
}
};
}

export function yjsProviderKeyPrefix(name: string): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/reflect-yjs/src/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class FakeReflect {
.fn()
.mockReturnValue(() => undefined);
mutate: {
updateYJS: MockedMutator<typeof updateYJS>;
updateYJS: MockedMutator<ReturnType<typeof updateYJS>>;
yjsSetLocalStateField: MockedMutator<typeof yjsSetLocalStateField>;
yjsSetLocalState: MockedMutator<typeof yjsSetLocalState>;
} = {
Expand Down

0 comments on commit 4ccaacc

Please sign in to comment.