Skip to content

Commit

Permalink
Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshuaKGoldberg committed Aug 30, 2020
1 parent ec8534b commit 6c26f5c
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 81 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ The site will now be available on http://localhost:3000.
### Documentation

- [Folders](./docs/Folders.md)
- [Room DOM](./docs/Room%20DOM.md)
- [Server Rooms](./docs/Server%20Rooms.md)
- [Client Room Context](./docs/Client%20Room%20Context.md)
- [Client Socket Messaging](./docs/Client%20Socket%20Messaging.md)

### Tech Stack

Expand Down
62 changes: 48 additions & 14 deletions docs/Client Room Context.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
# Client Room Context

Within the [`Room` page](/src/pages/Room/index.tsx), general room data is stored in a `RoomContext` React Context.
Code areas for setting up client connections to the server are contained in the [`@connection/` folder](./Folders.md#connection).
They are provided with basic room settings by the Room page, set up [React Contexts](https://reactjs.org/docs/context.html) that provide rudimentary connection APIs, then render the Room page as children underneath those contexts.

## Initializing Room State
Initializing the context consists of three phases, each corresponding to a React component that renders the next step when ready:

Room context state is initialized at the high-level [`RoomContainer` component](/src/pages/room/RoomContainer.tsx).
1. `DynamicScene`
2. `DynamicSceneHydrating`
3. `DynamicSceneConnected`

Each member is stored as a piece of `useState` state.
They're each wrapped with an instanceof the `GetterAndSetter` type, which is a generic TypeScript type that provides `.get()` and `.set()` methods.
### `DynamicScene`

> See [`RoomContext.ts`](/src/pages/room/RoomContext.ts) for more details on how the initial context value is created.
[`DynamicScene`](/src/connection/DynamicScene/index.tsx) receives basic settings directly passed by the page query and calls [`useRoomConnection`](/src/connection/RoomConnection.ts) to:

1. Initialze the NAF connection by setting the `networked-scene` attribute on the root Aframe scene, providing a `globalOnConnectHook` to NAF's `onConnect` pointing to our temporarily created global listener.
2. Upon connection success in that global listener, stores NAF's Socket.IO client in our React state.

That stored socket client is used by the rest of the connection logic to send data to and receive data from the server.

### `DynamicSceneHydrating`

[`DynamicSceneHydrating`](/src/connection/DynamicSceneHydrating/index.tsx) takes in that socket and calls [`useRoomDataConnection`](/src/connection/DynamicScene/DynamicSceneHydrating/useRoomDataConnection.ts) to:

1. Send a `KaraokeEvent.RoomDataHydration` event to the server requesting a summary of room information.
2. Upon receiving the response, store the room information in React state.

That stored room data is used by the rest of the connection and room logic to keep a local copy of the most recent snapshot of the room's state.

### `DynamicSceneConnected`

[`DynamicSceneConnected`](/src/connection/DynamicSceneConnected/index.tsx) receives accumulated client and room data state, and uses them to initialize contexts used by the rest room logic:

- [`EmitContext`](/src/connection/EmitContext.ts) receives a single `emit` function clients may send events to the server through.
- [`RoomContext`](/src/connection/RoomContext.ts) receives [`GetterAndSetter`] wrappers around room data, as well as an [`emitRoomData`] utility to simultaneously update local room data and send any changed values to the server.

It also starts a [`useRoomDataSyncing`](/src/connection/DynamicScene/DynamicSceneConnected/useRoomDataSyncing.ts) effect that continuously synchronizes local state to the server as it changes:

- It immediately emits a `KaraokeEvent.UsernameSet` to indicate the user's selected username to the server.
- When a `KaraokeEvent.OccupantsUpdated` event is received, it updates the local occupancy map.
- When a `KaraokeEvent.RoomDataUpdated` event is received, it updates the local room data.

## Reading Room State

Expand All @@ -26,22 +55,27 @@ The commonly used `roomData` member is a full object, so it's convenient to dest

```ts
const { roomData } = useRoomContext();
const { environment } = roomData.get();
const { currentTime } = roomData.get();

environment; // string
currentTime; // string
```

## Writing Room State

The `.set()` method on those members takes in a new version to pass to a `setState` setter.

```ts
const { roomData } = useRoomContext();
const { occupants } = useRoomContext();

roomData.set({
...roomData.get(),
environment: "new environment",
});
occupants.set(newOccupancyMap);
```

> 🔜 Consider reading [Client Socket Messaging](./Client%20Socket%20Messaging.md) next to understand how pages are synchronized.
For `RoomData` specifically, use the returned `emitRoomData` function to simultaneously update local room data and send any changed values to the server.

```ts
const { emitRoomData, roomData } = useRoomContext();

emitRoomData({
songs: [...roomData.get().songs, newSong],
});
```
46 changes: 0 additions & 46 deletions docs/Client Socket Messaging.md

This file was deleted.

8 changes: 8 additions & 0 deletions docs/Folders.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Application code for Karaoke Nite lives entirely within a few folders inside the `src/` directory:

- `components`
- `connection`
- `pages`
- `shared`
- `server`
Expand All @@ -19,6 +20,13 @@ These form a sort of fledgling component library that fills out our basic button

> 💡 Sticking to our fledgling design system makes it easier to keep pieces of the UI looking consistent and reduces work recreating similar components.
## `connection`

Room-specific logic for initialization a Socket.IO connection to the server and communicating over it.

- 👍 **Do** keep all code around initializing server connections here, to abstract those details away from other folders.
- 👎 **Don't** update UI based on the results of those connections here; prefer doing that in page-specific React components.

## `pages`

[Next.js Pages](https://nextjs.org/docs/basic-features/pages) are stored here.
Expand Down
26 changes: 26 additions & 0 deletions docs/Room DOM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Room DOM

Because Aframe uses direct DOM elements as a source of truth and the Room page uses Aframe for its 3D environment, we must manage direct DOM interactions in our React code.

> We tried [Aframe-React](https://github.com/supermedium/aframe-react) but ran into severe issues ([#5](https://github.com/karaokenite/karaokenite-react/issues/5); [#7](https://github.com/karaokenite/karaokenite-react/issues/7); [networked-aframe#226](https://github.com/networked-aframe/networked-aframe/issues/226)).
## Static HTML

We use a [Next.js custom `Document`](https://nextjs.org/docs/advanced-features/custom-document) to include:

- Static Aframe library scripts:

- When on the `/room` page, these are executed synchronously in a proper order.
- When on any other page, they are loaded as `async` as a prefetching technique.

- Raw Aframe elements on the page:

- Placing then in the raw HTML ensures they already exist on the page by the time our scripts need them.
- Our Aframe DOM template is stored in a [`template.html`](/src/pages/room/RoomBody/template.html) and loaded via Webpack's [`html-loader`](https://webpack.js.org/loaders/html-loader).

## DOM Interactions

Room DOM interactions are initialized by [`RoomEvents`](/src/pages/room/RoomEvents.ts), a `null`-returning component that sets React effect hooks to initialize DOM event listeners.
Those listeners generally use [`react-use`'s `useEvent`](https://github.com/streamich/react-use/blob/master/docs/useEvent.md) to be automatically disposed when needed.

> See [Client Room Context](./Client%20Room%20Context.md) for how those listeners interact with room state.
2 changes: 2 additions & 0 deletions docs/Server Rooms.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ We define our own events on top of the required NAF ones.

Clients may send these events to the server:

- `KaraokeEvent.RoomDataHydration`: One-time event requesting room data upon joining a room.
- `KaraokeEvent.RoomDataUpdated`: General update for some portion of a room's "jukebox" data, such as song index or volume.
- `KaraokeEvent.UsernameSet`: Adds a `username` alias for a person by id.

In response, the server may send these events back to the entire room:

- `KaraokeEvent.RoomDataHydration`: Provides a one-time full summary of room data upon joining a room.
- `KaraokeEvent.OccupantsUpdated`: Provides the full list of all occupants in the room.
- `KaraokeEvent.RoomDataUpdated`: Matching event to send the entire room's jukebox data.

Expand Down
9 changes: 2 additions & 7 deletions src/components/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const environmentSelector = "[environment]";

export const modalsElementId = "modals";

export const rootElementId = "root";
Expand All @@ -13,10 +15,3 @@ export const controlIds = {
volumeHighButton: "volume-high-button",
volumeLowButton: "volume-low-button",
};

export const globalOnConnectHook = "KaraokeNiteListenToConnection";

/**
* How often to send currentTime updates for videos.
*/
export const videoElementSyncInterval = 1000;
14 changes: 9 additions & 5 deletions src/components/elements.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Entity } from "aframe";
import { mapValues } from "lodash";

import { controlIds, sceneSelector, videoElementId } from "./constants";
import * as constants from "./constants";
import { getElementById, querySelector } from "./queries";
import { Entity } from "aframe";

export const sceneElement = querySelector<Entity>(sceneSelector);
export const environmentElement = querySelector<Entity>(
constants.environmentSelector
);

export const sceneElement = querySelector<Entity>(constants.sceneSelector);

export const videoElement = getElementById<Entity & HTMLVideoElement>(
videoElementId
constants.videoElementId
);

export const controls = mapValues(controlIds, getElementById);
export const controls = mapValues(constants.controlIds, getElementById);
6 changes: 6 additions & 0 deletions src/components/queries.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
Quick wrappers around DOM query methods with (unsafe) generic types for returns.
The document isn't available in server-side rendering but we want these elements
to be easily available in scripts that manipualte them.
*/

export const getElementById = <ElementType = HTMLElement>(id: string) => {
return (globalThis.document?.getElementById(id) as unknown) as ElementType;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ export const useRoomDataConnection = (socket: SocketIOClient.Socket) => {
const [roomData, setRoomData] = useState<RoomData>();

useEffect(() => {
const disconnect = () => {
const stopListening = () => {
socket.off(KaraokeEvent.RoomDataHydration, receiveRoomData);
};

const receiveRoomData = (data: RoomData) => {
setRoomData(data);
disconnect();
stopListening();
};

setRoomData(undefined);
socket.emit(KaraokeEvent.RoomDataHydration);
socket.on(KaraokeEvent.RoomDataHydration, receiveRoomData);

return disconnect;
return stopListening;
}, [socket]);

return roomData;
Expand Down
3 changes: 2 additions & 1 deletion src/connection/useRoomConnection.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useState } from "react";

import { globalOnConnectHook } from "@components/constants";
import { sceneElement } from "@components/elements";

const globalOnConnectHook = "KaraokeNiteListenToConnection";

export const useRoomConnection = (roomName: string) => {
const [socket, setSocket] = useState<SocketIOClient.Socket>();

Expand Down
2 changes: 0 additions & 2 deletions src/pages/room/RoomBody/RawHtml.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from "react";

// https://github.com/networked-aframe/networked-aframe/issues/226
// Ugh.
export const RawHtml = (html: string) => {
return function GeneratedRawHtml() {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useEffect } from "react";
import { useInterval } from "react-use";

import { videoElementSyncInterval } from "@components/constants";
import { videoElement } from "@components/elements";
import { useRoomContext } from "@connection/RoomContext";

/**
* How often to emit currentTime updates.
*/
const videoElementSyncInterval = 1000;

export const useTimeSynchronization = () => {
const { emitRoomData, roomData } = useRoomContext();
const { currentTime, playing } = roomData.get();
Expand Down
2 changes: 1 addition & 1 deletion src/shared/rooms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RoomData } from "./types";
import { defaultSongIndex } from "./songs";
import { RoomData } from "./types";

export const environments = [
"osiris",
Expand Down

0 comments on commit 6c26f5c

Please sign in to comment.