@roster-lock/match-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +11 -0
- package/dist/authentication.js +53 -0
- package/dist/authentication.js.map +1 -0
- package/dist/config/constants.js +12 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/findFile.js +32 -0
- package/dist/config/findFile.js.map +1 -0
- package/dist/config/index.js +29 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manage.js +41 -0
- package/dist/config/manage.js.map +1 -0
- package/dist/cors.js +20 -0
- package/dist/cors.js.map +1 -0
- package/dist/editor-support/index.js +247 -0
- package/dist/editor-support/index.js.map +1 -0
- package/dist/handle-room/version-1/file-routes.js +53 -0
- package/dist/handle-room/version-1/file-routes.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js +76 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js +218 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js +16 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js +370 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js +3 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js +95 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/index.js +18 -0
- package/dist/handle-room/version-1/globals/Room/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/room.js +3 -0
- package/dist/handle-room/version-1/globals/Room/room.js.map +1 -0
- package/dist/handle-room/version-1/globals/index.js +20 -0
- package/dist/handle-room/version-1/globals/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/types.js +3 -0
- package/dist/handle-room/version-1/globals/types.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js +83 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/index.js +35 -0
- package/dist/handle-room/version-1/handleDownloads/index.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/types.js +3 -0
- package/dist/handle-room/version-1/handleDownloads/types.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js +21 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js +12 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js.map +1 -0
- package/dist/handle-room/version-1/index.js +28 -0
- package/dist/handle-room/version-1/index.js.map +1 -0
- package/dist/handle-room/version-1/piece-sort.js +71 -0
- package/dist/handle-room/version-1/piece-sort.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js +89 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js +19 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js +170 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js +28 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js.map +1 -0
- package/dist/handle-room/version-1/schema/count.js +16 -0
- package/dist/handle-room/version-1/schema/count.js.map +1 -0
- package/dist/handle-room/version-1/schema/lock.js +46 -0
- package/dist/handle-room/version-1/schema/lock.js.map +1 -0
- package/dist/handle-room/version-1/schema/selected.js +13 -0
- package/dist/handle-room/version-1/schema/selected.js.map +1 -0
- package/dist/handle-room/version-1/select.js +94 -0
- package/dist/handle-room/version-1/select.js.map +1 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +88 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/fs.js +32 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/http-router.js +49 -0
- package/dist/utils/http-router.js.map +1 -0
- package/dist/utils/pagination.js +23 -0
- package/dist/utils/pagination.js.map +1 -0
- package/dist/utils/websocket-router.js +15 -0
- package/dist/utils/websocket-router.js.map +1 -0
- package/package.json +66 -0
- package/src/authentication.ts +48 -0
- package/src/config/constants.ts +7 -0
- package/src/config/findFile.ts +34 -0
- package/src/config/index.ts +17 -0
- package/src/config/manage.ts +48 -0
- package/src/cors.ts +23 -0
- package/src/editor-support/index.ts +289 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/index.ts +109 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/isUpToDate.ts +78 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/downloadSingleAsset.ts +129 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/example.ts +116 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/index.ts +144 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/types.ts +32 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/validateAssetFile.ts +54 -0
- package/src/handle-room/version-0/piece-download/index.ts +5 -0
- package/src/handle-room/version-0/piece-selection/constants.ts +39 -0
- package/src/handle-room/version-0/piece-selection/handleRoomSelections.ts +202 -0
- package/src/handle-room/version-0/piece-selection/index.ts +31 -0
- package/src/handle-room/version-0/piece-selection/roomErrors.ts +25 -0
- package/src/handle-room/version-0/piece-selection/steps/encrypton.ts +70 -0
- package/src/handle-room/version-0/piece-selection/steps/final-selection.ts +72 -0
- package/src/handle-room/version-0/piece-selection/steps/player-selection.ts +100 -0
- package/src/handle-room/version-1/constants.ts +44 -0
- package/src/handle-room/version-1/file-routes.ts +68 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.ts +83 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.ts +317 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.ts +31 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.ts +13 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.ts +547 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/types.ts +6 -0
- package/src/handle-room/version-1/globals/FolderDB/index.ts +3 -0
- package/src/handle-room/version-1/globals/FolderDB/types.ts +72 -0
- package/src/handle-room/version-1/globals/Room/WebsocketRoom.ts +118 -0
- package/src/handle-room/version-1/globals/Room/index.ts +3 -0
- package/src/handle-room/version-1/globals/Room/room.ts +8 -0
- package/src/handle-room/version-1/globals/index.ts +4 -0
- package/src/handle-room/version-1/globals/types.ts +7 -0
- package/src/handle-room/version-1/handleDownloads/DownloadTracker.ts +98 -0
- package/src/handle-room/version-1/handleDownloads/index.ts +41 -0
- package/src/handle-room/version-1/handleDownloads/types.ts +9 -0
- package/src/handle-room/version-1/handleRoomSelections/encryption.ts +29 -0
- package/src/handle-room/version-1/handleRoomSelections/random.ts +9 -0
- package/src/handle-room/version-1/index.ts +33 -0
- package/src/handle-room/version-1/piece-sort.ts +96 -0
- package/src/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.ts +125 -0
- package/src/handle-room/version-1/room-handler-bridge/http-handler.ts +16 -0
- package/src/handle-room/version-1/room-handler-bridge/server.pseudo.ts +85 -0
- package/src/handle-room/version-1/room-handler-bridge/steps.ts +255 -0
- package/src/handle-room/version-1/room-handler-bridge/ws-handler.ts +30 -0
- package/src/handle-room/version-1/room-handler-state/RoomState.ts +132 -0
- package/src/handle-room/version-1/room-handler-state/bind-to-websocket.ts +93 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomSelections.ts +71 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomWithErrors.ts +55 -0
- package/src/handle-room/version-1/room-handler-state/roomErrors.ts +35 -0
- package/src/handle-room/version-1/room-handler-state/user-selection.ts +98 -0
- package/src/handle-room/version-1/schema/count.ts +13 -0
- package/src/handle-room/version-1/schema/lock.ts +47 -0
- package/src/handle-room/version-1/schema/selected.ts +14 -0
- package/src/handle-room/version-1/select.ts +130 -0
- package/src/handle-room/version-1/validateUnknown.ts +11 -0
- package/src/index.old.ts +71 -0
- package/src/index.ts +231 -0
- package/src/server.ts +85 -0
- package/src/utils/fs.ts +43 -0
- package/src/utils/http-router.ts +55 -0
- package/src/utils/pagination.ts +17 -0
- package/src/utils/websocket-router.ts +23 -0
- package/tests/integration/editor-pieces-search.test.ts +147 -0
- package/tests/integration/game-complete.test.ts +72 -0
- package/tests/integration/handle-full-selection.test.ts +143 -0
- package/tests/integration/helpers/bridge.ts +22 -0
- package/tests/integration/helpers/fakeFolderDB.ts +37 -0
- package/tests/integration/helpers/fakePluginRuntime.ts +46 -0
- package/tests/integration/helpers/lockConfig.ts +71 -0
- package/tests/integration/helpers/make-tar-gz.ts +41 -0
- package/tests/integration/helpers/piece.ts +98 -0
- package/tests/integration/helpers/plugin-dir.ts +26 -0
- package/tests/integration/helpers/room.ts +42 -0
- package/tests/integration/helpers/server.ts +51 -0
- package/tests/integration/helpers/validLockConfig.ts +80 -0
- package/tests/integration/piece-download-and-list.test.ts +194 -0
- package/tests/integration/piece-ensure-ws.test.ts +256 -0
- package/tests/integration/piece-files.test.ts +139 -0
- package/tests/integration/piece-sort-list.test.ts +113 -0
- package/tests/integration/protocol.test.ts +129 -0
- package/tests/integration/root-and-auth.test.ts +100 -0
- package/tests/integration/sync-dl.test.ts +62 -0
- package/tests/tsconfig.json +14 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { MessageBridge } from "../../../utils/MessageBridge";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
|
|
4
|
+
type UserPublicKey = string;
|
|
5
|
+
|
|
6
|
+
export async function runRooms(users: Array<{ bridge: MessageBridge, publicKey: UserPublicKey }>){
|
|
7
|
+
const pingAbortController = new AbortController();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await Promise.race([
|
|
11
|
+
runRoom(users),
|
|
12
|
+
Promise.race(users.map(user=>keepPing(user.bridge, pingAbortController.signal))),
|
|
13
|
+
Promise.race(users.map(user=>waitForError(user.bridge))),
|
|
14
|
+
])
|
|
15
|
+
}catch(e){
|
|
16
|
+
for(const user of users){
|
|
17
|
+
user.bridge.sendEvent("error", (e as Error).message);
|
|
18
|
+
}
|
|
19
|
+
throw e;
|
|
20
|
+
}finally{
|
|
21
|
+
pingAbortController.abort();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function waitForError(user: MessageBridge){
|
|
26
|
+
return new Promise((resolve, reject)=>{
|
|
27
|
+
user.onEvent("error", (error)=>{
|
|
28
|
+
reject(new Error(error.toString()));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async function keepPing(user: MessageBridge, abortSignal: AbortSignal){
|
|
35
|
+
while(!abortSignal.aborted){
|
|
36
|
+
const result = await user.sendRequest("ping", {});
|
|
37
|
+
if(result !== "pong") throw new Error("Invalid Response");
|
|
38
|
+
await delay(1000);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function delay(ms: number){
|
|
43
|
+
return new Promise(resolve=>setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const UNSET_FINAL = Symbol("unset-final");
|
|
47
|
+
const INVALID_FINAL = Symbol("invalid-final");
|
|
48
|
+
async function runRoom(users: Array<{ bridge: MessageBridge, publicKey: UserPublicKey }>){
|
|
49
|
+
const userCommits: Record<UserPublicKey, any> = {};
|
|
50
|
+
await Promise.all(users.map(async (user)=>{
|
|
51
|
+
const commit = await user.bridge.sendRequest("user-selection", {});
|
|
52
|
+
userCommits[user.publicKey] = commit;
|
|
53
|
+
}));
|
|
54
|
+
const userReveals: Record<UserPublicKey, any> = {};
|
|
55
|
+
await Promise.all(users.map(async (user)=>{
|
|
56
|
+
const reveal = await user.bridge.sendRequest("all-selection-for-user-decryption", userCommits);
|
|
57
|
+
userReveals[user.publicKey] = reveal;
|
|
58
|
+
}));
|
|
59
|
+
let userFinals: typeof UNSET_FINAL | typeof INVALID_FINAL | Record<string, any> = UNSET_FINAL;
|
|
60
|
+
await Promise.all(users.map(async (user)=>{
|
|
61
|
+
const final = await user.bridge.sendRequest("all-decryption-for-user-final", userReveals);
|
|
62
|
+
if(userFinals === INVALID_FINAL) return;
|
|
63
|
+
if(userFinals === UNSET_FINAL){
|
|
64
|
+
userFinals = final;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if(!isDeepStrictEqual(userFinals, final)){
|
|
68
|
+
userFinals = INVALID_FINAL;
|
|
69
|
+
throw new Error("Final Selection Mismatch");
|
|
70
|
+
}
|
|
71
|
+
}));
|
|
72
|
+
for(const user of users){
|
|
73
|
+
user.bridge.onEvent("download-progress", (data)=>{
|
|
74
|
+
for(const otherUser of users){
|
|
75
|
+
otherUser.bridge.sendEvent("download-progress", { ...data, user: user.publicKey });
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
await Promise.all(users.map(async (user)=>{
|
|
80
|
+
await user.bridge.sendRequest("user-download", {});
|
|
81
|
+
}));
|
|
82
|
+
for(const user of users){
|
|
83
|
+
user.bridge.sendEvent("all-download", {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
|
|
2
|
+
import { MessageBridge } from "@roster-lock/utils";
|
|
3
|
+
import {
|
|
4
|
+
FinalSelection, RosterLockV1Config, SelectedPiece, UserSelection, PlayerId,
|
|
5
|
+
RosterLockV1SyncDLResult
|
|
6
|
+
} from "@roster-lock/types";
|
|
7
|
+
import { runSelection } from "@roster-lock/shared";
|
|
8
|
+
import { encryptJSON, decryptJSON } from "../handleRoomSelections/encryption";
|
|
9
|
+
import { createRandomSeed } from "../handleRoomSelections/random";
|
|
10
|
+
import { z, ZodType } from "zod";
|
|
11
|
+
import { UserSelectionSchema } from "../schema/selected";
|
|
12
|
+
import { PluginManager } from "@roster-lock/plugin-runtime";
|
|
13
|
+
import { handleDownloads } from "../handleDownloads";
|
|
14
|
+
import { IFolderDB } from "../globals/FolderDB";
|
|
15
|
+
import { RosterLockDownloadUpdate } from "@roster-lock/types";
|
|
16
|
+
|
|
17
|
+
type MachinePublicKey = string;
|
|
18
|
+
|
|
19
|
+
type EncryptedValue = Awaited<ReturnType<typeof encryptJSON>>;
|
|
20
|
+
|
|
21
|
+
type RoomState = (
|
|
22
|
+
| { state: "user-selection" }
|
|
23
|
+
| { state: "all-selection-for-user-decryption" }
|
|
24
|
+
| { state: "all-decryption-for-user-final", encrypted: Record<MachinePublicKey, EncryptedValue["encryptedValue"]> }
|
|
25
|
+
| { state: "user-download", finalSelection: FinalSelection }
|
|
26
|
+
| { state: "all-download", finalSelection: FinalSelection, downloadResults: Awaited<ReturnType<typeof handleDownloads>> }
|
|
27
|
+
| { state: "ended" }
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
type RoomArgs = {
|
|
31
|
+
bridge: MessageBridge,
|
|
32
|
+
fileDB: IFolderDB,
|
|
33
|
+
pluginRuntime: PluginManager,
|
|
34
|
+
machines: Array<{ publicKey: MachinePublicKey, playerCount: number }>,
|
|
35
|
+
ownMachinePublicKey: MachinePublicKey,
|
|
36
|
+
// This machine's local players' selections, keyed by local player index
|
|
37
|
+
// (0-based). A machine with multiple controllers plugged in submits one
|
|
38
|
+
// entry per local player over its single relay connection.
|
|
39
|
+
ownSelections: Record<number, UserSelection>,
|
|
40
|
+
lockConfig: RosterLockV1Config,
|
|
41
|
+
gameControlledSelections: Record<string, Array<SelectedPiece> | Record<PlayerId, Array<SelectedPiece>>>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ProgressListeners = Partial<{
|
|
45
|
+
onState: (state: string)=>void,
|
|
46
|
+
onDownloadProgress: (update: RosterLockDownloadUpdate)=>void,
|
|
47
|
+
}>
|
|
48
|
+
|
|
49
|
+
type UserInput = {
|
|
50
|
+
userSelection: UserSelection,
|
|
51
|
+
randomSeed: string,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// What one machine actually sends over the wire: all of its local players'
|
|
55
|
+
// selections plus the one random seed it contributes to the room.
|
|
56
|
+
type MachinePayload = {
|
|
57
|
+
selections: Record<number, UserSelection>,
|
|
58
|
+
randomSeed: string,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function bindStepsToBridge(
|
|
62
|
+
{
|
|
63
|
+
bridge,
|
|
64
|
+
fileDB,
|
|
65
|
+
pluginRuntime,
|
|
66
|
+
machines,
|
|
67
|
+
ownMachinePublicKey,
|
|
68
|
+
ownSelections,
|
|
69
|
+
lockConfig,
|
|
70
|
+
gameControlledSelections,
|
|
71
|
+
}: RoomArgs,
|
|
72
|
+
progressListeners: ProgressListeners = {}
|
|
73
|
+
): Promise<RosterLockV1SyncDLResult>{
|
|
74
|
+
const { resolve, reject, promise } = Promise.withResolvers<RosterLockV1SyncDLResult>();
|
|
75
|
+
const downloadAbort = new AbortController();
|
|
76
|
+
const heartbeat = trackHeartbeat(30_000);
|
|
77
|
+
const stateTracker = new StateTracker({ state: "user-selection" }, progressListeners);
|
|
78
|
+
|
|
79
|
+
const rngSeed = createRandomSeed();
|
|
80
|
+
const ownEncrypted = encryptJSON({ selections: ownSelections, randomSeed: rngSeed });
|
|
81
|
+
// Filled in by "all-decryption-for-user-final" below, read back by the
|
|
82
|
+
// handleFullSelection hook once the whole room promise settles.
|
|
83
|
+
let decryptedSelections: Record<PlayerId, UserInput> = {};
|
|
84
|
+
|
|
85
|
+
bridge.onRequest("ping", ()=>{
|
|
86
|
+
heartbeat.heartBeat();
|
|
87
|
+
return "pong";
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Return Encrypted Selections
|
|
91
|
+
bridge.onRequest("user-selection", async ()=>{
|
|
92
|
+
heartbeat.heartBeat();
|
|
93
|
+
const state = stateTracker.get();
|
|
94
|
+
if(state.state !== "user-selection") throw new Error("Invalid State");
|
|
95
|
+
const { encryptedValue } = await ownEncrypted;
|
|
96
|
+
stateTracker.set({ state: "all-selection-for-user-decryption" });
|
|
97
|
+
return encryptedValue;
|
|
98
|
+
});
|
|
99
|
+
// Handle All Selections
|
|
100
|
+
// Return Decryption Key
|
|
101
|
+
bridge.onRequest("all-selection-for-user-decryption", async (data)=>{
|
|
102
|
+
heartbeat.heartBeat();
|
|
103
|
+
const state = stateTracker.get();
|
|
104
|
+
if(state.state !== "all-selection-for-user-decryption") throw new Error("Invalid State");
|
|
105
|
+
const casted = EncryptedSelectionSchema.safeParse(data);
|
|
106
|
+
if(!casted.success) throw new Error("Invalid Selections");
|
|
107
|
+
if(Object.keys(casted.data).length !== machines.length) throw new Error("Invalid Machine Count");
|
|
108
|
+
for(const machine of machines){
|
|
109
|
+
if(!casted.data[machine.publicKey]) throw new Error("Missing Machine Selections");
|
|
110
|
+
}
|
|
111
|
+
const { key } = await ownEncrypted;
|
|
112
|
+
stateTracker.set({ state: "all-decryption-for-user-final", encrypted: casted.data });
|
|
113
|
+
return key;
|
|
114
|
+
})
|
|
115
|
+
// Handle All Decryption Keys
|
|
116
|
+
// Return Final Selections
|
|
117
|
+
bridge.onRequest("all-decryption-for-user-final", async (data)=>{
|
|
118
|
+
heartbeat.heartBeat();
|
|
119
|
+
const state = stateTracker.get();
|
|
120
|
+
if(state.state !== "all-decryption-for-user-final") throw new Error("Invalid State");
|
|
121
|
+
const allEncrypted = state.encrypted;
|
|
122
|
+
const casted = DecryptionKeySchema.safeParse(data);
|
|
123
|
+
if(!casted.success) throw new Error("Invalid Decryption Keys");
|
|
124
|
+
if(Object.keys(casted.data).length !== machines.length) throw new Error("Invalid Machine Count");
|
|
125
|
+
const keys = casted.data;
|
|
126
|
+
await Promise.all(machines.map(async (machine)=>{
|
|
127
|
+
const encrypted = allEncrypted[machine.publicKey];
|
|
128
|
+
if(!encrypted) throw new Error("Missing Encrypted Selections");
|
|
129
|
+
const key = keys[machine.publicKey];
|
|
130
|
+
if(!key) throw new Error("Missing Decryption Key");
|
|
131
|
+
const decrypted = await decryptJSON(key, encrypted);
|
|
132
|
+
const casted = MachinePayloadSchema.safeParse(decrypted);
|
|
133
|
+
if(!casted.success) throw new Error("Invalid Selections");
|
|
134
|
+
const { selections, randomSeed } = casted.data;
|
|
135
|
+
// A machine can't submit more or fewer player selections than it
|
|
136
|
+
// declared to the matchmaker when the room was created.
|
|
137
|
+
if(Object.keys(selections).length !== machine.playerCount){
|
|
138
|
+
throw new Error("Player Count Mismatch");
|
|
139
|
+
}
|
|
140
|
+
for(const [index, userSelection] of Object.entries(selections)){
|
|
141
|
+
decryptedSelections[`${machine.publicKey}:${index}`] = { userSelection, randomSeed };
|
|
142
|
+
}
|
|
143
|
+
}))
|
|
144
|
+
const finalSelection = await runSelection(
|
|
145
|
+
lockConfig, gameControlledSelections, decryptedSelections, (script)=>(pluginRuntime.runUntrustedScript(script))
|
|
146
|
+
);
|
|
147
|
+
stateTracker.set({ state: "user-download", finalSelection: finalSelection });
|
|
148
|
+
|
|
149
|
+
return finalSelection;
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
bridge.onRequest("user-download", async ()=>{
|
|
153
|
+
heartbeat.heartBeat();
|
|
154
|
+
const state = stateTracker.get();
|
|
155
|
+
if(state.state !== "user-download") throw new Error("Invalid State");
|
|
156
|
+
const downloadResults = await handleDownloads(
|
|
157
|
+
fileDB, lockConfig, state.finalSelection, {
|
|
158
|
+
onProgress: (event)=>{bridge.sendEvent("download-progress", event)},
|
|
159
|
+
abortSignal: downloadAbort.signal,
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
stateTracker.set({ state: "all-download", finalSelection: state.finalSelection, downloadResults });
|
|
163
|
+
return "ok"
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
bridge.onEvent("download-progress", (data: RosterLockDownloadUpdate)=>{
|
|
167
|
+
heartbeat.heartBeat();
|
|
168
|
+
progressListeners.onDownloadProgress?.(data);
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
bridge.onEvent("all-download", (data)=>{
|
|
172
|
+
heartbeat.heartBeat();
|
|
173
|
+
const state = stateTracker.get();
|
|
174
|
+
if(state.state !== "all-download") throw new Error("Invalid State");
|
|
175
|
+
resolve({
|
|
176
|
+
finalSelection: state.finalSelection,
|
|
177
|
+
downloadResults: state.downloadResults,
|
|
178
|
+
});
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
bridge.onEvent("error", (error)=>{
|
|
182
|
+
console.error("room error", error);
|
|
183
|
+
downloadAbort.abort();
|
|
184
|
+
reject(error);
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// .finally()'s returned promise mirrors promise's rejection - it has to
|
|
188
|
+
// be observed here too, or a failed room leaves an unhandled rejection.
|
|
189
|
+
promise.finally(()=>{
|
|
190
|
+
heartbeat.stop();
|
|
191
|
+
stateTracker.set({ state: "ended" });
|
|
192
|
+
}).catch(()=>{});
|
|
193
|
+
|
|
194
|
+
// Only fires once the whole room succeeds (selection + downloads) - doesn't
|
|
195
|
+
// block "all-decryption-for-user-final"'s response, and a broken/misbehaving
|
|
196
|
+
// indexing plugin can't stop the room, so failures here are only logged.
|
|
197
|
+
promise.then(()=>{
|
|
198
|
+
const userSelections = Object.fromEntries(
|
|
199
|
+
Object.entries(decryptedSelections).map(([playerId, input])=>[playerId, input.userSelection])
|
|
200
|
+
);
|
|
201
|
+
const localPlayers = Object.keys(ownSelections).map((index)=>`${ownMachinePublicKey}:${index}`);
|
|
202
|
+
return pluginRuntime.pieceSort.handleFullSelection({ lockConfig, localUsers: localPlayers, userSelections });
|
|
203
|
+
}).catch((e)=>{
|
|
204
|
+
console.error("piece-selection-sort handleFullSelection failed", e);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return Promise.race([
|
|
208
|
+
promise,
|
|
209
|
+
heartbeat.promise.then(()=>{
|
|
210
|
+
bridge.sendEvent("error", "Heartbeat Timeout");
|
|
211
|
+
downloadAbort.abort();
|
|
212
|
+
throw new Error("Heartbeat Timeout")
|
|
213
|
+
})
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const EncryptedSelectionSchema: ZodType<Record<MachinePublicKey, EncryptedValue["encryptedValue"]>> = z.record(z.string(), z.string());
|
|
218
|
+
|
|
219
|
+
const DecryptionKeySchema: ZodType<Record<MachinePublicKey, string>> = z.record(z.string(), z.string());
|
|
220
|
+
|
|
221
|
+
const MachinePayloadSchema: ZodType<MachinePayload> = z.object({
|
|
222
|
+
selections: z.record(z.string(), UserSelectionSchema),
|
|
223
|
+
randomSeed: z.string(),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
function trackHeartbeat(maxTimeout: number){
|
|
228
|
+
const { resolve, promise } = Promise.withResolvers<void>();
|
|
229
|
+
let to = setTimeout(resolve, maxTimeout);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
promise,
|
|
233
|
+
heartBeat: ()=>{
|
|
234
|
+
clearTimeout(to);
|
|
235
|
+
to = setTimeout(resolve, maxTimeout);
|
|
236
|
+
},
|
|
237
|
+
stop: ()=>{
|
|
238
|
+
clearTimeout(to);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
class StateTracker {
|
|
244
|
+
constructor(
|
|
245
|
+
private state: RoomState,
|
|
246
|
+
private listeners: ProgressListeners,
|
|
247
|
+
){}
|
|
248
|
+
set(newState: RoomState){
|
|
249
|
+
this.state = newState;
|
|
250
|
+
this.listeners.onState?.(newState.state);
|
|
251
|
+
}
|
|
252
|
+
get(){
|
|
253
|
+
return this.state;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { MessageBridge } from "@roster-lock/utils";
|
|
4
|
+
import { WebSocketHandlerCallback } from "../../../utils/websocket-router";
|
|
5
|
+
import { castRoomRequest, exchangeAndDownloadSelections } from "./exchange-and-download-selections";
|
|
6
|
+
import { V1Env } from "../globals/types";
|
|
7
|
+
|
|
8
|
+
export const wsHandler: WebSocketHandlerCallback = async function(
|
|
9
|
+
this: V1Env, { ws: gameWebSocket }, params, next
|
|
10
|
+
){
|
|
11
|
+
const { fileDB, pluginRuntime } = this;
|
|
12
|
+
try {
|
|
13
|
+
const gameBridge = new MessageBridge((message)=>gameWebSocket.send(JSON.stringify(message)));
|
|
14
|
+
gameWebSocket.on("message", (message)=>{
|
|
15
|
+
gameBridge.handleMessage(JSON.parse(message.toString()));
|
|
16
|
+
});
|
|
17
|
+
gameBridge.onRequest("connect-to-relay", async (roomInfo)=>{
|
|
18
|
+
const roomRequest = castRoomRequest(roomInfo);
|
|
19
|
+
return await exchangeAndDownloadSelections(fileDB, pluginRuntime, roomRequest, {
|
|
20
|
+
onState: (state)=>{gameBridge.sendEvent("room-state", state)},
|
|
21
|
+
onDownloadProgress: (event)=>{gameBridge.sendEvent("download-progress", event)},
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
gameBridge.sendEvent("ready", {});
|
|
25
|
+
}catch(e){
|
|
26
|
+
gameWebSocket.terminate();
|
|
27
|
+
next(e)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JSON_Unknown,
|
|
3
|
+
} from "@roster-lock/utils";
|
|
4
|
+
import {
|
|
5
|
+
RosterLockV1Config,
|
|
6
|
+
FinalSelection,
|
|
7
|
+
UserInput,
|
|
8
|
+
} from "@roster-lock/types";
|
|
9
|
+
import { MATCHLOCK_SELECTION_STATE, UserMessegeError } from "../constants";
|
|
10
|
+
import { IRoom } from "../globals/Room";
|
|
11
|
+
import { deepEqual } from "node:assert";
|
|
12
|
+
import { validateUnknown } from "../validateUnknown";
|
|
13
|
+
|
|
14
|
+
import { decryptJSON, encryptJSON, encryptedSchema } from "../handleRoomSelections/encryption";
|
|
15
|
+
import { userInputSchema, finalizeSelection, finalSelectionSchema } from "./user-selection";
|
|
16
|
+
import { PluginHandler } from "@roster-lock/plugin-runtime";
|
|
17
|
+
|
|
18
|
+
const STATE_ORDER = [
|
|
19
|
+
MATCHLOCK_SELECTION_STATE.hello,
|
|
20
|
+
MATCHLOCK_SELECTION_STATE.lockConfig,
|
|
21
|
+
MATCHLOCK_SELECTION_STATE.selectionEncrypt,
|
|
22
|
+
MATCHLOCK_SELECTION_STATE.selectionDecrypt,
|
|
23
|
+
MATCHLOCK_SELECTION_STATE.selectionFinal,
|
|
24
|
+
MATCHLOCK_SELECTION_STATE.goodbye,
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export class RoomState {
|
|
28
|
+
private state: MATCHLOCK_SELECTION_STATE = MATCHLOCK_SELECTION_STATE.hello;
|
|
29
|
+
private recievedMessages = new Map<MATCHLOCK_SELECTION_STATE, Set<string>>();
|
|
30
|
+
|
|
31
|
+
encryptedData = new Map<string, string>();
|
|
32
|
+
userSelections = new Map<string, UserInput>();
|
|
33
|
+
agreedSelection: FinalSelection | null = null;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
public room: IRoom,
|
|
37
|
+
public lockConfig: RosterLockV1Config,
|
|
38
|
+
public ownEncrypted: Awaited<ReturnType<typeof encryptJSON>>,
|
|
39
|
+
public pluginRuntime: PluginHandler
|
|
40
|
+
){}
|
|
41
|
+
|
|
42
|
+
validateMessageState(
|
|
43
|
+
userId: string,
|
|
44
|
+
messageState: MATCHLOCK_SELECTION_STATE
|
|
45
|
+
){
|
|
46
|
+
if(this.state !== messageState){
|
|
47
|
+
const currentIndex = STATE_ORDER.indexOf(this.state);
|
|
48
|
+
const messageIndex = STATE_ORDER.indexOf(messageState);
|
|
49
|
+
if(messageIndex === currentIndex + 1){
|
|
50
|
+
console.warn(`User Sent ${messageState} Early`, userId);
|
|
51
|
+
console.warn("We'll still process it as other messages may just be late");
|
|
52
|
+
} else {
|
|
53
|
+
throw new UserMessegeError(userId, `Expected ${this.state} message`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if(!this.recievedMessages.has(messageState)){
|
|
57
|
+
this.recievedMessages.set(messageState, new Set());
|
|
58
|
+
}
|
|
59
|
+
if(this.recievedMessages.get(messageState)!.has(userId)){
|
|
60
|
+
throw new UserMessegeError(userId, "User Sent Message Twice");
|
|
61
|
+
}
|
|
62
|
+
this.recievedMessages.get(messageState)!.add(userId);
|
|
63
|
+
return this.recievedMessages.get(messageState)!.size;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateStep(){
|
|
67
|
+
const currentIndex = STATE_ORDER.indexOf(this.state);
|
|
68
|
+
const nextIndex = currentIndex + 1;
|
|
69
|
+
if (nextIndex >= STATE_ORDER.length) {
|
|
70
|
+
throw new Error("No next phase after " + this.state);
|
|
71
|
+
}
|
|
72
|
+
this.state = STATE_ORDER[nextIndex];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stateHandlers: Record<string, {
|
|
76
|
+
message?(userId: string, data: JSON_Unknown):any,
|
|
77
|
+
finished():any
|
|
78
|
+
}> = {
|
|
79
|
+
[MATCHLOCK_SELECTION_STATE.hello]: {
|
|
80
|
+
finished: ()=>(
|
|
81
|
+
this.room.broadcast(MATCHLOCK_SELECTION_STATE.lockConfig, this.lockConfig)
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
[MATCHLOCK_SELECTION_STATE.lockConfig]: {
|
|
85
|
+
message: (userId, data)=>{
|
|
86
|
+
deepEqual(data, this.lockConfig, "Restriction Mismatch")
|
|
87
|
+
},
|
|
88
|
+
finished: ()=>(
|
|
89
|
+
this.room.broadcast(MATCHLOCK_SELECTION_STATE.selectionEncrypt, this.ownEncrypted.encryptedValue)
|
|
90
|
+
)
|
|
91
|
+
},
|
|
92
|
+
[MATCHLOCK_SELECTION_STATE.selectionEncrypt]: {
|
|
93
|
+
message: (userId, data)=>{
|
|
94
|
+
const casted = validateUnknown(encryptedSchema, data);
|
|
95
|
+
this.encryptedData.set(userId, casted);
|
|
96
|
+
},
|
|
97
|
+
finished: ()=>(
|
|
98
|
+
this.room.broadcast(MATCHLOCK_SELECTION_STATE.selectionDecrypt, this.ownEncrypted.key)
|
|
99
|
+
)
|
|
100
|
+
},
|
|
101
|
+
[MATCHLOCK_SELECTION_STATE.selectionDecrypt]: {
|
|
102
|
+
message: async (userId, data)=>{
|
|
103
|
+
if(typeof data !== "string") throw new Error("Invalid Decryption Key");
|
|
104
|
+
const encrypted = this.encryptedData.get(userId);
|
|
105
|
+
if(!encrypted) throw new Error("Missing Encrypted Data");
|
|
106
|
+
const decrypted = await decryptJSON(data, encrypted);
|
|
107
|
+
const casted = await validateUnknown(userInputSchema, decrypted);
|
|
108
|
+
this.userSelections.set(userId, casted);
|
|
109
|
+
},
|
|
110
|
+
finished: async ()=>{
|
|
111
|
+
const ownFinalSelection = await finalizeSelection(
|
|
112
|
+
this.lockConfig, {}, Object.fromEntries(this.userSelections), this.pluginRuntime
|
|
113
|
+
);
|
|
114
|
+
this.agreedSelection = ownFinalSelection;
|
|
115
|
+
this.room.broadcast(MATCHLOCK_SELECTION_STATE.selectionFinal, ownFinalSelection)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
[MATCHLOCK_SELECTION_STATE.selectionFinal]: {
|
|
119
|
+
message: (userId, data)=>{
|
|
120
|
+
const casted: FinalSelection = validateUnknown(finalSelectionSchema, data);
|
|
121
|
+
deepEqual(this.agreedSelection, casted, "Final Selection Mismatch");
|
|
122
|
+
},
|
|
123
|
+
finished: ()=>(
|
|
124
|
+
this.room.broadcast(MATCHLOCK_SELECTION_STATE.goodbye, "goodbye")
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
[MATCHLOCK_SELECTION_STATE.goodbye]: {
|
|
128
|
+
finished: ()=>{},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { UserInput } from "@roster-lock/types";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
|
|
3
|
+
import { z, ZodType } from "zod";
|
|
4
|
+
import { getSQLite3FolderDB } from "../globals/FolderDB";
|
|
5
|
+
import { WebsocketRoom } from "../globals/Room";
|
|
6
|
+
import { MessageBridge } from "@roster-lock/utils";
|
|
7
|
+
import { WebSocket } from "ws";
|
|
8
|
+
import { tryToHandleRoomVersion1 } from "./handleRoomWithErrors";
|
|
9
|
+
import { UserSelectionSchema } from "../schema/selected";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
Flow
|
|
13
|
+
- user interacts with matchmaker
|
|
14
|
+
- matchmaker creates room in relay server
|
|
15
|
+
- matchmaker provides room id to user
|
|
16
|
+
- user connects to room
|
|
17
|
+
- How does the relay room know it's the user
|
|
18
|
+
- Signed GET parameter?
|
|
19
|
+
- User needs to know all the other users public keys
|
|
20
|
+
- User needs to know their own private key
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
type RoomRequest = {
|
|
25
|
+
folder: string,
|
|
26
|
+
relay: {
|
|
27
|
+
url: string,
|
|
28
|
+
roomId: string,
|
|
29
|
+
},
|
|
30
|
+
user: {
|
|
31
|
+
timestamp: number,
|
|
32
|
+
publicKey: string,
|
|
33
|
+
signature: string,
|
|
34
|
+
},
|
|
35
|
+
rosterConfig: any,
|
|
36
|
+
userSelection: UserInput["userSelection"],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
const RequestCaster: ZodType<RoomRequest> = z.object({
|
|
42
|
+
folder: z.string(),
|
|
43
|
+
relay: z.object({
|
|
44
|
+
url: z.string(),
|
|
45
|
+
roomId: z.string(),
|
|
46
|
+
}),
|
|
47
|
+
user: z.object({
|
|
48
|
+
timestamp: z.number(),
|
|
49
|
+
publicKey: z.string(),
|
|
50
|
+
signature: z.string(),
|
|
51
|
+
}),
|
|
52
|
+
rosterConfig: z.any(),
|
|
53
|
+
userSelection: UserSelectionSchema,
|
|
54
|
+
}).strict();
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export const bindRawStateToWebsocket = async (gameWebSocket: WebSocket)=>{
|
|
58
|
+
const messageBridge = new MessageBridge((message)=>gameWebSocket.send(JSON.stringify(message)));
|
|
59
|
+
gameWebSocket.on("message", (message)=>{
|
|
60
|
+
messageBridge.handleMessage(JSON.parse(message.toString()));
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const roomInfo = await messageBridge.sendRequest("roomInfo", {});
|
|
64
|
+
|
|
65
|
+
const parsed = RequestCaster.safeParse(roomInfo);
|
|
66
|
+
if(!parsed.success) throw new Error("Invalid request");
|
|
67
|
+
const roomRequest = parsed.data;
|
|
68
|
+
const config = ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(roomRequest.rosterConfig, true);
|
|
69
|
+
const db = getSQLite3FolderDB(roomRequest.folder);
|
|
70
|
+
|
|
71
|
+
const room = new WebsocketRoom(roomRequest.relay, roomRequest.user);
|
|
72
|
+
await room.readyPromise;
|
|
73
|
+
|
|
74
|
+
const results = await tryToHandleRoomVersion1(db, room, config, roomRequest.userSelection);
|
|
75
|
+
|
|
76
|
+
await messageBridge.sendRequest("roomComplete", results);
|
|
77
|
+
room.disconnect();
|
|
78
|
+
}catch(e){
|
|
79
|
+
console.error(e);
|
|
80
|
+
await messageBridge.sendRequest("roomFailed", (e as Error).message);
|
|
81
|
+
}finally{
|
|
82
|
+
gameWebSocket.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class WebsocketMessageBridge extends MessageBridge {
|
|
87
|
+
constructor(private websocket: WebSocket){
|
|
88
|
+
super((message)=>websocket.send(JSON.stringify(message)));
|
|
89
|
+
this.websocket.on("message", (message)=>{
|
|
90
|
+
this.handleMessage(JSON.parse(message.toString()));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { MATCHLOCK_SELECTION_STATE } from "../constants";
|
|
2
|
+
import {
|
|
3
|
+
encryptJSON, decryptJSON,
|
|
4
|
+
} from "../handleRoomSelections/encryption";
|
|
5
|
+
import { createRandomSeed } from "../handleRoomSelections/random";
|
|
6
|
+
import { RosterLockV1Config, UserInput, FinalSelection } from "@roster-lock/types";
|
|
7
|
+
import { IRoom } from "../globals/Room/room";
|
|
8
|
+
import { RoomState } from "./RoomState";
|
|
9
|
+
|
|
10
|
+
export async function handleRoomSelections(
|
|
11
|
+
room: IRoom,
|
|
12
|
+
abortSignal: AbortSignal,
|
|
13
|
+
restriction: RosterLockV1Config,
|
|
14
|
+
selection: UserInput["userSelection"],
|
|
15
|
+
){
|
|
16
|
+
|
|
17
|
+
const rngSeed = createRandomSeed();
|
|
18
|
+
const ownEncrypted = await encryptJSON({ selection, rngSeed })
|
|
19
|
+
|
|
20
|
+
if(abortSignal.aborted){
|
|
21
|
+
return Promise.reject(new Error("Aborted"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const roomState = new RoomState(
|
|
25
|
+
room, restriction, ownEncrypted
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const { promise, resolve, reject } = Promise.withResolvers<FinalSelection>();
|
|
29
|
+
|
|
30
|
+
// Send our "hello" after we start listening
|
|
31
|
+
Promise.resolve().then(async ()=>{
|
|
32
|
+
room.broadcast(MATCHLOCK_SELECTION_STATE.hello, "hello");
|
|
33
|
+
});
|
|
34
|
+
const stopListeningToRoom = room.listen(async (userId, event, data)=>{
|
|
35
|
+
try {
|
|
36
|
+
if(!(event in roomState.stateHandlers )){
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// ensure the message wasn't sent twice and event is in order
|
|
40
|
+
const count = roomState.validateMessageState(userId, event as MATCHLOCK_SELECTION_STATE);
|
|
41
|
+
const { message, finished } = roomState.stateHandlers[event];
|
|
42
|
+
// If there is a message handler, run it
|
|
43
|
+
await message?.(userId, data);
|
|
44
|
+
// If we haven't heard from everyone, exit
|
|
45
|
+
if(count < room.numberOfUsers) return;
|
|
46
|
+
// Update the room step
|
|
47
|
+
roomState.updateStep();
|
|
48
|
+
// Run the finished handler which includes broadcasting the next step
|
|
49
|
+
await finished();
|
|
50
|
+
|
|
51
|
+
// If we're in the goodbye state, we're done
|
|
52
|
+
if(event !== MATCHLOCK_SELECTION_STATE.goodbye) return;
|
|
53
|
+
if(!roomState.agreedSelection) throw new Error("Missing Agreed FInal Selection");
|
|
54
|
+
resolve(roomState.agreedSelection!);
|
|
55
|
+
}catch(e){
|
|
56
|
+
reject(e);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
promise.finally(()=>{
|
|
61
|
+
stopListeningToRoom();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
abortSignal.addEventListener("abort", ()=>(
|
|
65
|
+
reject(new Error("Aborted"))
|
|
66
|
+
));
|
|
67
|
+
|
|
68
|
+
return promise;
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|