Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic ws server #903

Merged
merged 35 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
30b29ab
Simple websocket server
ddimaria Nov 29, 2023
07a6739
Reorganize and remove unwraps
ddimaria Nov 30, 2023
1e939ac
Improve NewRoom message, add MouseMove message
ddimaria Nov 30, 2023
3814882
Add app-level state to the server
ddimaria Nov 30, 2023
5d95595
Add dotenv and envy for env var support
ddimaria Nov 30, 2023
b97633d
Store users in a room and return room upon enter
ddimaria Nov 30, 2023
afbe889
Store a socket connection for each user in a room
ddimaria Nov 30, 2023
1c4ba81
Broadcast Room message to all users in a room
ddimaria Nov 30, 2023
bc90f03
Genericize broadcasting + add README
ddimaria Nov 30, 2023
03e9346
Fix bug in MouseMove
ddimaria Nov 30, 2023
de36c92
Add integration test harness, test user enter a room
ddimaria Dec 1, 2023
de6e9d0
Merge remote-tracking branch 'origin/a-team' into basic-ws-server
ddimaria Dec 1, 2023
ba26889
Add package.json
ddimaria Dec 1, 2023
a70aebf
Document all the things
ddimaria Dec 1, 2023
5c8d1cf
Refactor and add tests
ddimaria Dec 1, 2023
648b1f2
Enhance the README
ddimaria Dec 1, 2023
4136a34
More tests, refactoring, add local coverage
ddimaria Dec 1, 2023
1fda2da
Broadcast messages in a separate thread
ddimaria Dec 1, 2023
5a01270
Don't await braodbast messages
ddimaria Dec 1, 2023
1fab67f
Add ws server to CI
ddimaria Dec 1, 2023
06c2334
Add .env.test for CI
ddimaria Dec 1, 2023
1002258
Conditionally load .env.test when testing
ddimaria Dec 2, 2023
5088c6e
basic multiplayer cursor
davidfig Dec 3, 2023
b3e8c5c
mulitplayer selection
davidfig Dec 3, 2023
ddc58bc
cursor disappears when other player is off the canvas
davidfig Dec 4, 2023
a60f578
added types to TS
davidfig Dec 4, 2023
e122ea0
added tests
davidfig Dec 4, 2023
0a001fa
removed a weird artifact
davidfig Dec 4, 2023
c1a55f2
added multiplayer start to npm start
davidfig Dec 4, 2023
793db5c
Merge branch 'a-team' into basic-ws-server
davidfig Dec 4, 2023
e09008c
Merge branch 'a-team' into basic-ws-server
davidfig Dec 4, 2023
69a665f
file_id is real now; started work on transactions
davidfig Dec 4, 2023
20d23f2
add zod to api-deps (needed for heroku)
davidkircos Dec 4, 2023
d5288ec
try api
davidkircos Dec 4, 2023
1d2b49b
ignore
davidkircos Dec 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions .github/workflows/coverage-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,22 @@ jobs:
rustup override set nightly
if ! which grcov; then cargo install grcov; fi

- name: Build
- name: Build quadratic-core
env:
RUSTFLAGS: -Cinstrument-coverage
run: |
cd quadratic-core
cargo build

- name: Test
- name: Test quadratic-core
env:
LLVM_PROFILE_FILE: grcov-%p-%m.profraw
RUSTFLAGS: -Cinstrument-coverage
run: |
cd quadratic-core
cargo test

- name: Generate coverage
- name: Generate coverage quadratic-core
run: |
grcov $(find . -name "grcov-*.profraw" -print) \
--branch \
Expand All @@ -60,7 +60,41 @@ jobs:
--ignore "./quadratic-core/src/bin/*" \
-o lcov.info

- name: Upload coverage reports to Codecov
- name: Upload coverage reports to Codecov quadratic-core
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

- name: Build quadratic-multiplayer
env:
RUSTFLAGS: -Cinstrument-coverage
run: |
cd quadratic-multiplayer
cargo build

- name: Test quadratic-multiplayer
env:
LLVM_PROFILE_FILE: grcov-%p-%m.profraw
RUSTFLAGS: -Cinstrument-coverage
run: |
cd quadratic-multiplayer
cargo test

- name: Generate coverage quadratic-multiplayer
run: |
grcov $(find . -name "grcov-*.profraw" -print) \
--branch \
--ignore-not-existing \
--binary-path ./quadratic-multiplayer/target/debug/ \
-s . \
-t lcov \
--ignore "/*" \
--ignore "./quadratic-multiplayer/src/wasm_bindings/*" \
--ignore "./quadratic-multiplayer/src/bin/*" \
-o lcov.info

- name: Upload coverage reports to Codecov quadratic-multiplayer
uses: codecov/codecov-action@v3
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ venv/*
# Generated Rust files
quadratic-core/target/
quadratic-core/tmp.txt
quadratic-multiplayer/target/

# Generated JS files
quadratic-api/node_modules/
Expand All @@ -45,3 +46,4 @@ quadratic-api/dist

# Code Coverage
quadratic-core/coverage
quadratic-multiplayer/coverage
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.linkedProjects": ["./quadratic-core/Cargo.toml"],
"rust-analyzer.linkedProjects": ["./quadratic-core/Cargo.toml", "./quadratic-multiplayer/Cargo.toml"],
"rust-analyzer.checkOnSave": true,
// "rust-analyzer.checkOnSave.command": "clippy",
"files.associations": {
Expand Down
11 changes: 10 additions & 1 deletion package-lock.json

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

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
"workspaces": [
"quadratic-api",
"quadratic-shared",
"quadratic-client"
"quadratic-client",
"quadratic-multiplayer"
],
"scripts": {
"start": "npm run watch:front-end",
"start:api": "npm start --workspace=quadratic-api",
"perf": "npm run watch:perf:front-end",
"watch:javascript": "cd quadratic-client && npm run watch:javascript",
"watch:perf:front-end": "npm run build:wasm:types && concurrently --hide=2 -n=react,rust,rust-types \"npm:watch:javascript\" \"npm:watch:wasm:perf:javascript\" \"npm run start:api\"",
"watch:front-end": "npm run build:wasm:types && concurrently --hide=2 -n=react,rust,rust-types \"npm:watch:javascript\" \"npm:watch:wasm:javascript\" \"npm run start:api\"",
"watch:front-end-back-end": "npm run build:wasm:types && concurrently -n=react,rust,api \"npm:watch:javascript\" \"npm:watch:wasm:javascript\" \"npm run start:api\"",
"watch:front-end": "npm run build:wasm:types && concurrently --hide=2 -n=react,rust,rust-types \"npm:watch:javascript\" \"npm:watch:wasm:javascript\" \"npm:start:api\" \"npm:watch:multiplayer\"",
"watch:front-end-back-end": "npm run build:wasm:types && concurrently -n=react,rust,api \"npm:watch:javascript\" \"npm:watch:wasm:javascript\" \"npm:start:api\" \"npm:watch:multiplayer\"",
"watch:app": "npm run build:wasm:types && concurrently -n=react,api 'npm:watch:javascript' 'npm run start:api'",
"watch:multiplayer": "npm run dev --workspace=quadratic-multiplayer",
"lint:client": "cd quadratic-client && npm run lint:ts && npm run lint:eslint && lint:prettier && lint:clippy",
"build:wasm:types": "cd quadratic-core && cargo run --bin export_types",
"watch:wasm:javascript": "cd quadratic-core && cargo watch -s 'wasm-pack build --dev --target web --out-dir ../quadratic-client/src/quadratic-core --weak-refs'",
Expand All @@ -30,7 +32,8 @@
"coverage:wasm:gen": "cd quadratic-core && cd quadratic-core && CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test",
"coverage:wasm:html": "cd quadratic-core && cd quadratic-core && grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore 'src/wasm_bindings/*' --ignore 'src/bin/*' --ignore '../*' --ignore '/*' -o coverage/html",
"coverage:wasm:view": "open quadratic-core/coverage/html/index.html",
"test:wasm": "cd quadratic-core && cargo test",
"test:wasm": "cd run test --workspace=quadratic-core",
"test:multiplayer": "npm run test --workspace=quadratic-multiplayer",
"watch:test:wasm": "cd quadratic-core && cargo watch -x test",
"benchmark:rust": "cd quadratic-core && cargo bench",
"lint:clippy": "cd quadratic-core && cargo clippy --all-targets --all-features -- -D warnings"
Expand Down
5 changes: 5 additions & 0 deletions quadratic-api/src/middleware/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import dbClient from '../dbClient';
import { Request, RequestWithTeam, RequestWithUser } from '../types/Request';
import { ResponseError } from '../types/Response';
import { getTeamAccess } from '../utils';
import { userMiddleware } from './user';
import { validateAccessToken } from './validateAccessToken';

const teamUuidSchema = z.string().uuid();

Expand All @@ -19,6 +21,9 @@ export const teamMiddleware = async (
res: Response<ResponseError>,
next: NextFunction
) => {
await validateAccessToken(req, res, () => {});
await userMiddleware(req, res, () => {});
davidkircos marked this conversation as resolved.
Show resolved Hide resolved

// Validate the team UUID
const teamUuid = req.params.uuid;
try {
Expand Down
9 changes: 8 additions & 1 deletion quadratic-client/src/dashboard/FileRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { multiplayer } from '@/multiplayer/multiplayer';
import { useRootRouteLoaderData } from '@/router';
import { Button } from '@/shadcn/ui/button';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import * as Sentry from '@sentry/react';
Expand All @@ -23,6 +25,7 @@ import QuadraticApp from '../ui/QuadraticApp';

export type FileData = {
name: string;
uuid: string;
sharing: ApiTypes['/v0/files/:uuid/sharing.GET.response'];
permission: ApiTypes['/v0/files/:uuid.GET.response']['permission'];
};
Expand Down Expand Up @@ -84,20 +87,24 @@ export const loader = async ({ request, params }: LoaderFunctionArgs): Promise<F

return {
name: data.file.name,
uuid: data.file.uuid,
permission: data.permission,
sharing,
};
};

export const Component = () => {
const { user } = useRootRouteLoaderData();

// Initialize recoil with the file's permission we get from the server
const { permission } = useLoaderData() as FileData;
const { permission, uuid } = useLoaderData() as FileData;
const initializeState = ({ set }: MutableSnapshot) => {
set(editorInteractionStateAtom, (prevState) => ({
...prevState,
permission,
}));
};
multiplayer.enterFileRoom(uuid, user);

return (
<RecoilRoot initializeState={initializeState}>
Expand Down
12 changes: 12 additions & 0 deletions quadratic-client/src/grid/sheet/SheetCursor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { multiplayer } from '@/multiplayer/multiplayer';
import { IViewportTransformState } from 'pixi-viewport';
import { Rectangle } from 'pixi.js';
import { pixiApp } from '../../gridGL/pixiApp/PixiApp';
Expand Down Expand Up @@ -77,6 +78,17 @@ export class SheetCursor {
this.keyboardMovePosition = options.keyboardMovePosition;
}
pixiApp.updateCursorPosition({ ensureVisible: options.ensureVisible ?? true });
multiplayer.sendSelection(
this.cursorPosition,
this.multiCursor
? new Rectangle(
this.multiCursor.originPosition.x,
this.multiCursor.originPosition.y,
this.multiCursor.terminalPosition.x - this.multiCursor.originPosition.x,
this.multiCursor.terminalPosition.y - this.multiCursor.originPosition.y
)
: undefined
);
}

changeBoxCells(boxCells: boolean) {
Expand Down
2 changes: 2 additions & 0 deletions quadratic-client/src/gridGL/QuadraticGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MultiplayerCursors } from '@/multiplayer/multiplayerCursor/MulitplayerCursors';
import { useCallback, useEffect, useState } from 'react';
import { useRecoilState } from 'recoil';
import { editorInteractionStateAtom } from '../atoms/editorInteractionStateAtom';
Expand Down Expand Up @@ -117,6 +118,7 @@ export default function QuadraticGrid() {
>
{showInput && <CellInput container={container} />}
<FloatingContextMenu container={container} showContextMenu={showContextMenu} />
<MultiplayerCursors />
</div>
);
}
73 changes: 73 additions & 0 deletions quadratic-client/src/gridGL/UI/UIMultiplayerCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { multiplayer } from '@/multiplayer/multiplayer';
import { MULTIPLAYER_COLORS_TINT } from '@/multiplayer/multiplayerCursor/multiplayerColors';
import { Graphics, Rectangle } from 'pixi.js';
import { sheets } from '../../grid/controller/Sheets';
import { pixiApp } from '../pixiApp/PixiApp';
import { Coordinate } from '../types/size';

export const CURSOR_THICKNESS = 1;
const ALPHA = 0.5;
const FILL_ALPHA = 0.01 / ALPHA;

export class UIMultiPlayerCursor extends Graphics {
dirty = false;

constructor() {
super();
this.alpha = ALPHA;
}

// todo: handle multiple people in the same cell
private drawCursor(color: number, cursor: Coordinate) {
const sheet = sheets.sheet;

let { x, y, width, height } = sheet.getCellOffsets(cursor.x, cursor.y);

// draw cursor
this.lineStyle({
width: CURSOR_THICKNESS,
color,
alignment: 0,
});
this.moveTo(x, y);
this.lineTo(x + width, y);
this.lineTo(x + width, y + height);
this.moveTo(x + width, y + height);
this.lineTo(x, y + height);
this.lineTo(x, y);
}

private drawMultiCursor(color: number, rectangle: Rectangle): void {
const sheet = sheets.sheet;
this.lineStyle(1, color, 1, 0, true);
this.beginFill(color, FILL_ALPHA);
const startCell = sheet.getCellOffsets(rectangle.x, rectangle.y);
const endCell = sheet.getCellOffsets(rectangle.x + rectangle.width, rectangle.y + rectangle.height);
this.drawRect(
startCell.x,
startCell.y,
endCell.x + endCell.width - startCell.x,
endCell.y + endCell.height - startCell.y
);
}

update() {
if (this.dirty) {
this.dirty = false;
this.clear();
// const sheetId = sheets.sheet.id;
multiplayer.players.forEach((player) => {
const color = MULTIPLAYER_COLORS_TINT[player.color];
if (player.selection /* && player.sheetId === sheetId */) {
this.drawCursor(color, player.selection.cursor);

// note: the rectangle is not really a PIXI.Rectangle, but a (x, y, width, height) type (b/c we JSON stringified)
if (player.selection.rectangle) {
this.drawMultiCursor(color, player.selection.rectangle);
}
}
});
pixiApp.setViewportDirty();
}
}
}
9 changes: 8 additions & 1 deletion quadratic-client/src/gridGL/interaction/pointer/Pointer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { multiplayer } from '@/multiplayer/multiplayer';
import { Viewport } from 'pixi-viewport';
import { InteractionEvent } from 'pixi.js';
import { pixiApp } from '../../pixiApp/PixiApp';
Expand All @@ -23,14 +24,20 @@ export class Pointer {
viewport.on('pointermove', this.pointerMove);
viewport.on('pointerup', this.pointerUp);
viewport.on('pointerupoutside', this.pointerUp);
pixiApp.canvas.addEventListener('pointerleave', this.pointerLeave);
}

private pointerLeave = () => {
multiplayer.sendMouseMove();
};

destroy() {
const viewport = pixiApp.viewport;
viewport.off('pointerdown', this.handlePointerDown);
viewport.off('pointermove', this.pointerMove);
viewport.off('pointerup', this.pointerUp);
viewport.off('pointerupoutside', this.pointerUp);
pixiApp.canvas.removeEventListener('pointerleave', this.pointerLeave);
davidkircos marked this conversation as resolved.
Show resolved Hide resolved
this.pointerDown.destroy();
}

Expand All @@ -54,7 +61,7 @@ export class Pointer {
this.pointerHeading.pointerMove(world) ||
this.pointerAutoComplete.pointerMove(world) ||
this.pointerDown.pointerMove(world);
this.pointerCursor.pointerMove();
this.pointerCursor.pointerMove(world);
};

private pointerUp = (e: InteractionEvent): void => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { multiplayer } from '@/multiplayer/multiplayer';
import { Point } from 'pixi.js';
import { pixiApp } from '../../pixiApp/PixiApp';

export class PointerCursor {
pointerMove(): void {
pointerMove(world: Point): void {
const cursor = pixiApp.pointer.pointerHeading.cursor ?? pixiApp.pointer.pointerAutoComplete.cursor;
pixiApp.canvas.style.cursor = cursor ?? 'unset';
multiplayer.sendMouseMove(world.x, world.y);
}
}
Loading
Loading