@@ -1,2 +1,3 @@ | |||
node_modules | |||
dist | |||
dist | |||
state |
@@ -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, | |||
}; | |||
}; | |||
@@ -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); | |||
@@ -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(); | |||
@@ -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; | |||
@@ -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 ( | |||
@@ -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); | |||
@@ -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, | |||
}); |
@@ -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>; | |||
@@ -33,7 +33,7 @@ const { | |||
processAction, | |||
receivedCompaction, | |||
} = getRoomStatesHandler(2, (roomName: string) => { | |||
const roomUsers = usersToRooms[roomName]; | |||
const roomUsers = roomsToUsers[roomName]; | |||
if (roomUsers.length === 0) { | |||
return; | |||
} | |||
@@ -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; | |||
} | |||
@@ -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, | |||