Browse Source

New reducer-like state handling.

master
Mattia Belletti 3 months ago
parent
commit
7fc5d0df78
12 changed files with 83 additions and 61 deletions
  1. +2
    -1
      .gitignore
  2. +12
    -16
      src/client/server-connection.ts
  3. +8
    -7
      src/client/start-plugins.ts
  4. +8
    -7
      src/client/state.ts
  5. +4
    -9
      src/client/use-state.ts
  6. +8
    -8
      src/plugins/chat/component.tsx
  7. +2
    -0
      src/plugins/chat/index.ts
  8. +20
    -0
      src/plugins/chat/reducer.ts
  9. +3
    -3
      src/plugins/plugins.d.ts
  10. +1
    -1
      src/server/index.ts
  11. +14
    -8
      src/server/persistence.ts
  12. +1
    -1
      src/server/state.ts

+ 2
- 1
.gitignore View File

@@ -1,2 +1,3 @@
node_modules
dist
dist
state

+ 12
- 16
src/client/server-connection.ts View File

@@ -3,6 +3,7 @@ import {
ServerToClientMessage,
ServerToRoomMessage,
Error,
Action,
} from "../common/message";
import {
setCurrentState,
@@ -11,8 +12,6 @@ import {
actionProcessed,
getCurrentState,
getLastProcessedActionId,
setOnStateChanged,
doAction,
} from "./state";

const noop = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
@@ -21,11 +20,9 @@ export interface ServerConnection {
onconnectionerror: (ev: Event) => void;
onprotocolerror: (e: Error) => void;
onenterroom: (participants: string[]) => void;
onstatechanged: (newState: State) => void;
state: State;
username: string | null;
roomName: string | null;
sendAction(action: PluginAction): void;
sendPerformAction(action: Action): void;
}

const createServerConnection = (
@@ -36,7 +33,6 @@ const createServerConnection = (
let onconnectionerror: (err: Event) => void = console.error;
let onprotocolerror: (e: Error) => void = console.error;
let onenterroom: (participants: string[]) => void = noop;
let currentRoom: string | null = null;
let username: string | null = null;
let roomName: string | null = null;

@@ -61,7 +57,6 @@ const createServerConnection = (
type: "enter-room",
roomName,
});
currentRoom = roomName;
};

webSocket.onerror = (err) => {
@@ -116,6 +111,15 @@ const createServerConnection = (
}
};

const sendPerformAction = (action: Action) => {
if (roomName !== null) {
sendMessage({
type: "perform-action",
roomName,
action,
});
}
};
return {
set onconnectionerror(handler: (ev: Event) => void) {
onconnectionerror = handler;
@@ -126,21 +130,13 @@ const createServerConnection = (
set onenterroom(handler: (participants: string[]) => void) {
onenterroom = handler;
},
set onstatechanged(handler: (newState: State) => void) {
setOnStateChanged(() => handler(getCurrentState()));
},
get state() {
return getCurrentState();
},
get username() {
return username;
},
get roomName() {
return roomName;
},
sendAction(action: PluginAction) {
doAction(action);
},
sendPerformAction,
};
};



+ 8
- 7
src/client/start-plugins.ts View File

@@ -9,7 +9,7 @@ import useRootPublicInterface from "./use-public-interface";
import registerRootCSS from "./css";
import useRootState from "./use-state";
import { getCapsRegister } from "./plugin-permissions";
import { State, setReducer } from "./state";
import { State, setReducer, setOnStateChanged, getCurrentState } from "./state";

declare function lockdown(): void;

@@ -40,9 +40,10 @@ export const initializePlugins = async (
).filter((x) => x !== null) as [string, string][];

const listeners: Array<(newState: State) => void> = [];
serverConnection.onstatechanged = (newState) => {
setOnStateChanged(() => {
const newState = getCurrentState();
listeners.forEach((l) => l(newState));
};
});
const addStateChangedCallback = (cb: (newState: State) => void) =>
listeners.push(cb);

@@ -66,11 +67,11 @@ export const initializePlugins = async (
}
};

const getRootState = (pluginName: string) =>
serverConnection.state[pluginName];
const getRootState = (pluginName: string) => () =>
getCurrentState()[pluginName];

const performRootAction = (pluginName: string) => (payload: unknown) => {
serverConnection.sendAction({
serverConnection.sendPerformAction({
id: uuid(),
data: {
plugin: pluginName,
@@ -106,7 +107,7 @@ export const initializePlugins = async (
registerCSS: registerRootCSS(name),
username: serverConnection.username,
roomName: serverConnection.roomName,
useState: useRootState(name, serverConnection, addStateChangedCallback),
useState: useRootState(name, addStateChangedCallback),
debug: (...args: unknown[]) => console.log(`${name}:`, ...args),
getCaps: permissionsRegister.getCaps,
}).evaluate(text);


+ 8
- 7
src/client/state.ts View File

@@ -35,9 +35,12 @@ export const setReducer = (
) => {
reducers[plugin] = reducer;
while (enqueuedActions.length > 0) {
console.log("processing");
if (enqueuedActions[0].data.plugin in reducers) {
applyAction(enqueuedActions[0]);
enqueuedActions.splice(1, 1);
enqueuedActions.splice(0, 1);
} else {
break;
}
}
};
@@ -52,7 +55,10 @@ const processAction = (action: PluginAction) => {

const applyAction = (action: PluginAction) => {
const { plugin, payload } = action.data;
currentState[plugin] = reducers[plugin](currentState[plugin], payload);
currentState[plugin] = reducers[plugin](
(currentState || {})[plugin],
payload
);
lastProcessedActionId = action.id;
};

@@ -65,11 +71,6 @@ export const setCurrentState = (
handleStateChanged();
};

export const doAction = (action: PluginAction) => {
processAction(action);
handleStateChanged();
};

export const actionProcessed = (action: PluginAction) => {
processAction(action);
handleStateChanged();


+ 4
- 9
src/client/use-state.ts View File

@@ -1,21 +1,16 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useEffect, useState } from "react";
import { ServerConnection } from "./server-connection";
import { State } from "./state-interface";
import { getCurrentState, State } from "./state";

const useRootState = (
name: string,
serverConnection: ServerConnection,
addStateChangedCallback: (cb: (newState: State) => void) => void
) => () => {
const [currentState, setCurrentState] = useState(
serverConnection.state && serverConnection.state[name]
);
const getState = (s: State) => s && s[name];
const [currentState, setCurrentState] = useState(getState(getCurrentState()));

useEffect(() => {
addStateChangedCallback((newState) =>
setCurrentState(newState && newState[name])
);
addStateChangedCallback((newState) => setCurrentState(getState(newState)));
}, []);

return currentState;


+ 8
- 8
src/plugins/chat/component.tsx View File

@@ -3,6 +3,7 @@ import { ChangeEvent, KeyboardEvent } from "react";
const { TextField, makeStyles } = MaterialUI;

import styles from "./chat.module.css";
import { Action } from "./reducer";

const pick = <T extends unknown, K extends keyof T>(
o: T,
@@ -41,7 +42,7 @@ const s = (...classes: string[]) => classes.join(" ");
const Chat = () => {
const themedStyles = useStyles();

const chatState = useState<Line[]>();
const chatState = useState<Line[]>() || [];

const [currentLine, setCurrentLine] = React.useState("");

@@ -52,16 +53,15 @@ const Chat = () => {
const onKeyDown = React.useCallback(
(ev: KeyboardEvent<HTMLInputElement>) => {
if (ev.key === "Enter") {
setState(
(chatState || []).concat({
username,
line: currentLine,
})
);
performAction<Action>({
type: "add-line",
username,
line: currentLine,
});
setCurrentLine("");
}
},
[currentLine, chatState]
[currentLine]
);

return (


+ 2
- 0
src/plugins/chat/index.ts View File

@@ -1,6 +1,8 @@
import BoardPublicInterface from "../board/public-interface";
import Chat from "./component";
import { reducer } from "./reducer";

setReducer(reducer);
getPublicInterface<BoardPublicInterface>("board").then(
(boardPublicInterface) => {
boardPublicInterface.addTab("Chat", Chat);


+ 20
- 0
src/plugins/chat/reducer.ts View File

@@ -0,0 +1,20 @@
export interface Line {
username: string;
line: string;
}

export type State = Line[];

export interface AddLineAction {
type: "add-line";
username: string;
line: string;
}

export type Action = AddLineAction;

export const reducer = (currentState: State, action: Action) =>
(currentState || []).concat({
username: action.username,
line: action.line,
});

+ 3
- 3
src/plugins/plugins.d.ts View File

@@ -9,9 +9,9 @@ declare global {
const MaterialUIIcons: typeof GlobalMaterialUIIcons;

function getState<S = unknown>(): S;
function performAction(action: unknown): void;
function setReducer(
reducer: (currentState: unknown, payload: unknown) => unknown
function performAction<P = unknown>(action: P): void;
function setReducer<S = unknown, P = unknown>(
reducer: (currentState: S, payload: P) => S
): void;
function useState<S = unknown>(): S;
function getPublicInterface<T = unknown>(name: string): Promise<T>;


+ 1
- 1
src/server/index.ts View File

@@ -33,7 +33,7 @@ const {
processAction,
receivedCompaction,
} = getRoomStatesHandler(2, (roomName: string) => {
const roomUsers = usersToRooms[roomName];
const roomUsers = roomsToUsers[roomName];
if (roomUsers.length === 0) {
return;
}


+ 14
- 8
src/server/persistence.ts View File

@@ -7,18 +7,22 @@ const getRootStateFname = (roomName: string) => `${roomName}-rootState.json`;
const getActionFname = (roomName: string, idx: number) =>
`${roomName}-action${idx}.json`;

const getPath = (fname: string) => path.resolve(__dirname, fname);
const getPath = (fname: string) => path.resolve(process.cwd(), "state", fname);

const readFile = <T = unknown>(fname: string): T =>
JSON.parse(fs.readFileSync(getPath(fname), { encoding: "utf-8" }));
const readFile = <T = unknown>(fname: string): T => {
const f = getPath(fname);
const data = fs.readFileSync(f, { encoding: "utf-8" });
return JSON.parse(data);
};

const writeFile = (fname: string, o: unknown) => {
fs.writeFileSync(getPath(fname), JSON.stringify(o, null, 2), {
const f = getPath(fname);
fs.writeFileSync(f, JSON.stringify(o, null, 2), {
encoding: "utf-8",
});
};

const deleteFile = (fname: string) => fs.unlinkSync(fname);
const deleteFile = (fname: string) => fs.unlinkSync(getPath(fname));

const exists = (fname: string) => fs.existsSync(getPath(fname));

@@ -36,15 +40,18 @@ export const compactState = (
actions: Action[]
) => {
// TODO: salvare copia per evitare che se si rompe a metà si perda lo stato
console.log("Removing current room state");
deleteFile(getRootStateFname(roomName));
for (let i = 0; ; i++) {
const actionFName = getActionFname(roomName, i);
if (exists(actionFName)) {
console.log("Removing action", i);
deleteFile(actionFName);
} else {
break;
}
}
console.log("Writing new state and actions");
writeFile(getRootStateFname(roomName), rootState);
actions.forEach((action, i) =>
writeFile(getActionFname(roomName, i), action)
@@ -54,8 +61,8 @@ export const compactState = (
export const loadState = (roomName: string): RoomState => {
const rootFName = getRootStateFname(roomName);
if (!exists(rootFName)) {
writeFile(rootFName, undefined);
return { rootState: undefined, actions: [] };
writeFile(rootFName, {});
return { rootState: {}, actions: [] };
}
const rootState = readFile(rootFName);
const actions: Action[] = [];
@@ -63,7 +70,6 @@ export const loadState = (roomName: string): RoomState => {
const actionFName = getActionFname(roomName, i);
if (exists(actionFName)) {
actions.push(readFile<Action>(actionFName));
deleteFile(actionFName);
} else {
break;
}


+ 1
- 1
src/server/state.ts View File

@@ -30,7 +30,7 @@ export const getRoomStatesHandler = (
const { actions } = getRoomState(roomName);
for (let i = actions.length - 1; i >= 0; i--) {
if (actions[i].id === lastCompactedActionId) {
const newActions = actions.slice(0, i + 1);
const newActions = actions.slice(i + 1);
compactState(roomName, compactedRoomState, newActions);
roomStates[roomName] = {
rootState: compactedRoomState,


Loading…
Cancel
Save