@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.
Files changed (174) hide show
  1. package/LICENSE.md +11 -0
  2. package/dist/authentication.js +53 -0
  3. package/dist/authentication.js.map +1 -0
  4. package/dist/config/constants.js +12 -0
  5. package/dist/config/constants.js.map +1 -0
  6. package/dist/config/findFile.js +32 -0
  7. package/dist/config/findFile.js.map +1 -0
  8. package/dist/config/index.js +29 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/config/manage.js +41 -0
  11. package/dist/config/manage.js.map +1 -0
  12. package/dist/cors.js +20 -0
  13. package/dist/cors.js.map +1 -0
  14. package/dist/editor-support/index.js +247 -0
  15. package/dist/editor-support/index.js.map +1 -0
  16. package/dist/handle-room/version-1/file-routes.js +53 -0
  17. package/dist/handle-room/version-1/file-routes.js.map +1 -0
  18. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js +76 -0
  19. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js.map +1 -0
  20. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js +218 -0
  21. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js.map +1 -0
  22. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js +18 -0
  23. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js.map +1 -0
  24. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js +16 -0
  25. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js.map +1 -0
  26. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js +370 -0
  27. package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js.map +1 -0
  28. package/dist/handle-room/version-1/globals/FolderDB/index.js +18 -0
  29. package/dist/handle-room/version-1/globals/FolderDB/index.js.map +1 -0
  30. package/dist/handle-room/version-1/globals/FolderDB/types.js +3 -0
  31. package/dist/handle-room/version-1/globals/FolderDB/types.js.map +1 -0
  32. package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js +95 -0
  33. package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js.map +1 -0
  34. package/dist/handle-room/version-1/globals/Room/index.js +18 -0
  35. package/dist/handle-room/version-1/globals/Room/index.js.map +1 -0
  36. package/dist/handle-room/version-1/globals/Room/room.js +3 -0
  37. package/dist/handle-room/version-1/globals/Room/room.js.map +1 -0
  38. package/dist/handle-room/version-1/globals/index.js +20 -0
  39. package/dist/handle-room/version-1/globals/index.js.map +1 -0
  40. package/dist/handle-room/version-1/globals/types.js +3 -0
  41. package/dist/handle-room/version-1/globals/types.js.map +1 -0
  42. package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js +83 -0
  43. package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js.map +1 -0
  44. package/dist/handle-room/version-1/handleDownloads/index.js +35 -0
  45. package/dist/handle-room/version-1/handleDownloads/index.js.map +1 -0
  46. package/dist/handle-room/version-1/handleDownloads/types.js +3 -0
  47. package/dist/handle-room/version-1/handleDownloads/types.js.map +1 -0
  48. package/dist/handle-room/version-1/handleRoomSelections/encryption.js +21 -0
  49. package/dist/handle-room/version-1/handleRoomSelections/encryption.js.map +1 -0
  50. package/dist/handle-room/version-1/handleRoomSelections/random.js +12 -0
  51. package/dist/handle-room/version-1/handleRoomSelections/random.js.map +1 -0
  52. package/dist/handle-room/version-1/index.js +28 -0
  53. package/dist/handle-room/version-1/index.js.map +1 -0
  54. package/dist/handle-room/version-1/piece-sort.js +71 -0
  55. package/dist/handle-room/version-1/piece-sort.js.map +1 -0
  56. package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js +89 -0
  57. package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js.map +1 -0
  58. package/dist/handle-room/version-1/room-handler-bridge/http-handler.js +19 -0
  59. package/dist/handle-room/version-1/room-handler-bridge/http-handler.js.map +1 -0
  60. package/dist/handle-room/version-1/room-handler-bridge/steps.js +170 -0
  61. package/dist/handle-room/version-1/room-handler-bridge/steps.js.map +1 -0
  62. package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js +28 -0
  63. package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js.map +1 -0
  64. package/dist/handle-room/version-1/schema/count.js +16 -0
  65. package/dist/handle-room/version-1/schema/count.js.map +1 -0
  66. package/dist/handle-room/version-1/schema/lock.js +46 -0
  67. package/dist/handle-room/version-1/schema/lock.js.map +1 -0
  68. package/dist/handle-room/version-1/schema/selected.js +13 -0
  69. package/dist/handle-room/version-1/schema/selected.js.map +1 -0
  70. package/dist/handle-room/version-1/select.js +94 -0
  71. package/dist/handle-room/version-1/select.js.map +1 -0
  72. package/dist/index.js +180 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/server.js +88 -0
  75. package/dist/server.js.map +1 -0
  76. package/dist/utils/fs.js +32 -0
  77. package/dist/utils/fs.js.map +1 -0
  78. package/dist/utils/http-router.js +49 -0
  79. package/dist/utils/http-router.js.map +1 -0
  80. package/dist/utils/pagination.js +23 -0
  81. package/dist/utils/pagination.js.map +1 -0
  82. package/dist/utils/websocket-router.js +15 -0
  83. package/dist/utils/websocket-router.js.map +1 -0
  84. package/package.json +66 -0
  85. package/src/authentication.ts +48 -0
  86. package/src/config/constants.ts +7 -0
  87. package/src/config/findFile.ts +34 -0
  88. package/src/config/index.ts +17 -0
  89. package/src/config/manage.ts +48 -0
  90. package/src/cors.ts +23 -0
  91. package/src/editor-support/index.ts +289 -0
  92. package/src/handle-room/version-0/piece-download/availableGamePiece/index.ts +109 -0
  93. package/src/handle-room/version-0/piece-download/availableGamePiece/isUpToDate.ts +78 -0
  94. package/src/handle-room/version-0/piece-download/downloadPieceAssets/downloadSingleAsset.ts +129 -0
  95. package/src/handle-room/version-0/piece-download/downloadPieceAssets/example.ts +116 -0
  96. package/src/handle-room/version-0/piece-download/downloadPieceAssets/index.ts +144 -0
  97. package/src/handle-room/version-0/piece-download/downloadPieceAssets/types.ts +32 -0
  98. package/src/handle-room/version-0/piece-download/downloadPieceAssets/validateAssetFile.ts +54 -0
  99. package/src/handle-room/version-0/piece-download/index.ts +5 -0
  100. package/src/handle-room/version-0/piece-selection/constants.ts +39 -0
  101. package/src/handle-room/version-0/piece-selection/handleRoomSelections.ts +202 -0
  102. package/src/handle-room/version-0/piece-selection/index.ts +31 -0
  103. package/src/handle-room/version-0/piece-selection/roomErrors.ts +25 -0
  104. package/src/handle-room/version-0/piece-selection/steps/encrypton.ts +70 -0
  105. package/src/handle-room/version-0/piece-selection/steps/final-selection.ts +72 -0
  106. package/src/handle-room/version-0/piece-selection/steps/player-selection.ts +100 -0
  107. package/src/handle-room/version-1/constants.ts +44 -0
  108. package/src/handle-room/version-1/file-routes.ts +68 -0
  109. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.ts +83 -0
  110. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.ts +317 -0
  111. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.ts +31 -0
  112. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.ts +13 -0
  113. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.ts +547 -0
  114. package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/types.ts +6 -0
  115. package/src/handle-room/version-1/globals/FolderDB/index.ts +3 -0
  116. package/src/handle-room/version-1/globals/FolderDB/types.ts +72 -0
  117. package/src/handle-room/version-1/globals/Room/WebsocketRoom.ts +118 -0
  118. package/src/handle-room/version-1/globals/Room/index.ts +3 -0
  119. package/src/handle-room/version-1/globals/Room/room.ts +8 -0
  120. package/src/handle-room/version-1/globals/index.ts +4 -0
  121. package/src/handle-room/version-1/globals/types.ts +7 -0
  122. package/src/handle-room/version-1/handleDownloads/DownloadTracker.ts +98 -0
  123. package/src/handle-room/version-1/handleDownloads/index.ts +41 -0
  124. package/src/handle-room/version-1/handleDownloads/types.ts +9 -0
  125. package/src/handle-room/version-1/handleRoomSelections/encryption.ts +29 -0
  126. package/src/handle-room/version-1/handleRoomSelections/random.ts +9 -0
  127. package/src/handle-room/version-1/index.ts +33 -0
  128. package/src/handle-room/version-1/piece-sort.ts +96 -0
  129. package/src/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.ts +125 -0
  130. package/src/handle-room/version-1/room-handler-bridge/http-handler.ts +16 -0
  131. package/src/handle-room/version-1/room-handler-bridge/server.pseudo.ts +85 -0
  132. package/src/handle-room/version-1/room-handler-bridge/steps.ts +255 -0
  133. package/src/handle-room/version-1/room-handler-bridge/ws-handler.ts +30 -0
  134. package/src/handle-room/version-1/room-handler-state/RoomState.ts +132 -0
  135. package/src/handle-room/version-1/room-handler-state/bind-to-websocket.ts +93 -0
  136. package/src/handle-room/version-1/room-handler-state/handleRoomSelections.ts +71 -0
  137. package/src/handle-room/version-1/room-handler-state/handleRoomWithErrors.ts +55 -0
  138. package/src/handle-room/version-1/room-handler-state/roomErrors.ts +35 -0
  139. package/src/handle-room/version-1/room-handler-state/user-selection.ts +98 -0
  140. package/src/handle-room/version-1/schema/count.ts +13 -0
  141. package/src/handle-room/version-1/schema/lock.ts +47 -0
  142. package/src/handle-room/version-1/schema/selected.ts +14 -0
  143. package/src/handle-room/version-1/select.ts +130 -0
  144. package/src/handle-room/version-1/validateUnknown.ts +11 -0
  145. package/src/index.old.ts +71 -0
  146. package/src/index.ts +231 -0
  147. package/src/server.ts +85 -0
  148. package/src/utils/fs.ts +43 -0
  149. package/src/utils/http-router.ts +55 -0
  150. package/src/utils/pagination.ts +17 -0
  151. package/src/utils/websocket-router.ts +23 -0
  152. package/tests/integration/editor-pieces-search.test.ts +147 -0
  153. package/tests/integration/game-complete.test.ts +72 -0
  154. package/tests/integration/handle-full-selection.test.ts +143 -0
  155. package/tests/integration/helpers/bridge.ts +22 -0
  156. package/tests/integration/helpers/fakeFolderDB.ts +37 -0
  157. package/tests/integration/helpers/fakePluginRuntime.ts +46 -0
  158. package/tests/integration/helpers/lockConfig.ts +71 -0
  159. package/tests/integration/helpers/make-tar-gz.ts +41 -0
  160. package/tests/integration/helpers/piece.ts +98 -0
  161. package/tests/integration/helpers/plugin-dir.ts +26 -0
  162. package/tests/integration/helpers/room.ts +42 -0
  163. package/tests/integration/helpers/server.ts +51 -0
  164. package/tests/integration/helpers/validLockConfig.ts +80 -0
  165. package/tests/integration/piece-download-and-list.test.ts +194 -0
  166. package/tests/integration/piece-ensure-ws.test.ts +256 -0
  167. package/tests/integration/piece-files.test.ts +139 -0
  168. package/tests/integration/piece-sort-list.test.ts +113 -0
  169. package/tests/integration/protocol.test.ts +129 -0
  170. package/tests/integration/root-and-auth.test.ts +100 -0
  171. package/tests/integration/sync-dl.test.ts +62 -0
  172. package/tests/tsconfig.json +14 -0
  173. package/tsconfig.json +22 -0
  174. package/vitest.config.ts +10 -0
@@ -0,0 +1,118 @@
1
+ import { createSimpleEmitter, JSON_Unknown, handleFetch, delay } from "@roster-lock/utils";
2
+ import { IRoom } from "./room";
3
+ import { WebSocket } from "ws";
4
+ import { once } from "events";
5
+
6
+ type RelayConfig = {
7
+ url: string,
8
+ roomId: string,
9
+ };
10
+
11
+ type UserConfig = {
12
+ timestamp: number,
13
+ publicKey: string,
14
+ signature: string,
15
+ };
16
+
17
+
18
+ type User = {
19
+ userId: string,
20
+ publicKey: string,
21
+ displayName: string,
22
+ };
23
+
24
+ type WebSocketRoomState = (
25
+ | { state: "connecting" }
26
+ | { state: "connected", websocket: WebSocket, users: Array<User> }
27
+ | { state: "destroyed" }
28
+ )
29
+
30
+ export class WebsocketRoom implements IRoom {
31
+ private state: WebSocketRoomState = { state: "connecting" };
32
+ private relayURL: URL;
33
+ private connectPromise: Promise<void>;
34
+ constructor(
35
+ private relayConfig: RelayConfig,
36
+ userConfig: UserConfig,
37
+ ){
38
+ const url = new URL(relayConfig.url);
39
+ url.searchParams.set("room", userConfig.signature);
40
+ url.searchParams.set("t", userConfig.timestamp.toString());
41
+ url.searchParams.set("pk", userConfig.publicKey);
42
+ url.searchParams.set("sig", userConfig.signature);
43
+ this.relayURL = url;
44
+ this.connectPromise = this.setup();
45
+ }
46
+
47
+ get readyPromise(){
48
+ return this.connectPromise;
49
+ }
50
+
51
+ private async setup(){
52
+ const { relayURL: url } = this;
53
+ const wsURL = new URL(url);
54
+ wsURL.protocol = url.protocol === "https:" ? "wss:" : "ws";
55
+ wsURL.pathname = "/" + this.relayConfig.roomId;
56
+ const websocket = new WebSocket(wsURL.href);
57
+
58
+ const abortController = new AbortController();
59
+ const httpURL = new URL(url);
60
+ httpURL.pathname = "/" + this.relayConfig.roomId + "/users";
61
+
62
+ try {
63
+ const [users] = await Promise.race([
64
+ Promise.all([
65
+ handleFetch<Array<User>>(fetch(httpURL.href, { signal: abortController.signal })),
66
+ once(websocket, 'open'),
67
+ ]),
68
+ Promise.resolve().then(async ()=>{
69
+ await delay(10_000);
70
+ throw new Error("Timeout");
71
+ })
72
+ ]);
73
+ this.state = { state: "connected", websocket, users };
74
+ }catch(e){
75
+ abortController.abort();
76
+ websocket.close();
77
+ this.state = { state: "destroyed" };
78
+ throw e;
79
+ }
80
+ }
81
+
82
+ get numberOfUsers(){
83
+ if(this.state.state !== "connected") throw new Error("Not Connected");
84
+ return this.state.users.length;
85
+ }
86
+ listen = createSimpleEmitter()
87
+ async broadcast(event: string, data: JSON_Unknown){
88
+ if(this.state.state !== "connected") throw new Error("Not Connected");
89
+ await this.sendMessage({
90
+ type: event,
91
+ payload: data,
92
+ });
93
+ }
94
+ disconnect = ()=>{
95
+ if(this.state.state === "destroyed") return;
96
+ if(this.state.state === "connecting"){
97
+ this.state = { state: "destroyed" };
98
+ return;
99
+ }
100
+ const { websocket } = this.state;
101
+ websocket.close();
102
+ this.state = { state: "destroyed" };
103
+ }
104
+
105
+ private sendMessage(message: any){
106
+ return new Promise<void>((resolve, reject)=>{
107
+ if(this.state.state !== "connected") return reject(new Error("Not Connected"));
108
+ this.state.websocket.send(JSON.stringify(message), (error) => {
109
+ if (error) {
110
+ reject(error);
111
+ } else {
112
+ resolve();
113
+ }
114
+ });
115
+ });
116
+ }
117
+ }
118
+
@@ -0,0 +1,3 @@
1
+
2
+ export { IRoom } from "./room";
3
+ export * from "./WebsocketRoom";
@@ -0,0 +1,8 @@
1
+ import { JSON_Unknown, ISimpleEventEmitter } from "@roster-lock/utils";
2
+
3
+ export interface IRoom {
4
+ numberOfUsers: number
5
+ listen: ISimpleEventEmitter<[playerId: string, event: string, data: JSON_Unknown]>
6
+ broadcast: (event: string, data: JSON_Unknown)=>Promise<void>
7
+ disconnect: ()=>void
8
+ }
@@ -0,0 +1,4 @@
1
+
2
+ export * from "./types";
3
+ export * from "./FolderDB";
4
+ export * from "./Room";
@@ -0,0 +1,7 @@
1
+
2
+ import { IFolderDB } from "./FolderDB";
3
+ import { PluginManager } from "@roster-lock/plugin-runtime";
4
+ export type V1Env = {
5
+ fileDB: IFolderDB,
6
+ pluginRuntime: PluginManager,
7
+ }
@@ -0,0 +1,98 @@
1
+ import { RosterLockV1Config, SelectedPiece, PieceType, PieceId } from "@roster-lock/types";
2
+ import { IFolderDB } from "../globals/FolderDB";
3
+ import { ProgressHandlers } from "./types";
4
+
5
+ import { DownloadResultsMap, DownloadResult } from "./types";
6
+ import { ROSTERLOCK_DOWNLOAD_STATE } from "@roster-lock/types";
7
+
8
+ export class PieceDownloadTracker {
9
+ private pieceTypes = new Map<PieceId, Map<PieceType, Promise<DownloadResult>>>();
10
+
11
+ constructor(
12
+ public database: IFolderDB,
13
+ public lockConfig: RosterLockV1Config,
14
+ public progressHandlers: ProgressHandlers,
15
+ ){}
16
+
17
+ public tryToDownloadAndNested(pieceType: PieceType, piece: SelectedPiece){
18
+ if(!this.isDownloadQueued(pieceType, piece.id)){
19
+ this.addNewDownload(
20
+ pieceType, piece.id,
21
+ tryToDownloadAnySource(this.database, this.lockConfig, pieceType, piece, this.progressHandlers)
22
+ );
23
+ }
24
+ // We don't skip required as different pieces may have different required pieces
25
+ for(const [requiredPieceType, requiredPieces] of Object.entries(piece.required)){
26
+ for(const requiredPiece of requiredPieces.mandatory){
27
+ this.tryToDownloadAndNested(requiredPieceType, requiredPiece);
28
+ }
29
+ for(const requiredPiece of requiredPieces.selectable){
30
+ this.tryToDownloadAndNested(requiredPieceType, requiredPiece);
31
+ }
32
+ }
33
+ }
34
+
35
+ private isDownloadQueued(pieceType: PieceType, pieceId: PieceId){
36
+ const pieces = this.pieceTypes.get(pieceType);
37
+ if(!pieces) return false;
38
+ return pieces.has(pieceId);
39
+ }
40
+ addNewDownload(pieceType: PieceType, pieceId: PieceId, promise: Promise<DownloadResult>){
41
+ if(!this.pieceTypes.has(pieceType)){
42
+ this.pieceTypes.set(pieceType, new Map());
43
+ }
44
+ this.pieceTypes.get(pieceType)!.set(pieceId, promise);
45
+ }
46
+ getDownloadPromises(){
47
+ return Array.from(this.pieceTypes.values()).flatMap(r=>Array.from(r.values()));
48
+ }
49
+
50
+ static resultsToMap(results: DownloadResult[]): DownloadResultsMap{
51
+ const typeMap: DownloadResultsMap = {};
52
+ for(const result of results){
53
+ const pieceMap: DownloadResultsMap[PieceType] = (()=>{
54
+ const map = typeMap[result.pieceType];
55
+ if(map) return map;
56
+ const newMap = {};
57
+ typeMap[result.pieceType] = newMap;
58
+ return newMap;
59
+ })();
60
+ pieceMap[result.pieceId] = result;
61
+ }
62
+ return typeMap;
63
+ }
64
+
65
+ }
66
+
67
+ async function tryToDownloadAnySource(
68
+ db: IFolderDB,
69
+ lockconfig: RosterLockV1Config,
70
+ pieceType: string,
71
+ selectedPiece: SelectedPiece,
72
+ progressHandlers: ProgressHandlers
73
+ ): Promise<DownloadResult>{
74
+ const pieces = lockconfig.rosters[pieceType];
75
+ if(!pieces){
76
+ throw new Error(`Missing Roster for piece type ${pieceType}`);
77
+ }
78
+ const piece = pieces.find(p=>p.id === selectedPiece.id);
79
+ if(!piece){
80
+ throw new Error(`Missing Piece ${selectedPiece.id} for piece type ${pieceType}`);
81
+ }
82
+
83
+ const pieceVersions = { logic: piece.version.logic, media: piece.version.media };
84
+ progressHandlers.onProgress({
85
+ type: ROSTERLOCK_DOWNLOAD_STATE.downloadStart,
86
+ pieceType,
87
+ pieceVersions,
88
+ });
89
+ const folder = await db.ensurePieceExists(
90
+ lockconfig.engine, pieceType, piece, progressHandlers
91
+ );
92
+ progressHandlers.onProgress({
93
+ type: ROSTERLOCK_DOWNLOAD_STATE.downloadFinished,
94
+ pieceType,
95
+ pieceVersions,
96
+ });
97
+ return { pieceType, pieceId: selectedPiece.id, pieceVersions, folder };
98
+ }
@@ -0,0 +1,41 @@
1
+ import { FinalSelection, RosterLockV1Config } from "@roster-lock/types";
2
+
3
+ import { PieceDownloadTracker } from "./DownloadTracker";
4
+ import { ROSTERLOCK_DOWNLOAD_STATE } from "@roster-lock/types";
5
+ import { IFolderDB } from "../globals/FolderDB";
6
+ import { ProgressHandlers, DownloadResultsMap } from "./types";
7
+
8
+ export async function handleDownloads(
9
+ db: IFolderDB,
10
+ lockConfig: RosterLockV1Config,
11
+ finalSelection: FinalSelection,
12
+ progressHandlers: ProgressHandlers,
13
+ ): Promise<DownloadResultsMap> {
14
+ try {
15
+ const downloadTracker = new PieceDownloadTracker(db, lockConfig, progressHandlers);
16
+ for(const [pieceType, selection] of Object.entries(finalSelection)){
17
+ if(selection.type === "shared"){
18
+ for(const piece of selection.value){
19
+ downloadTracker.tryToDownloadAndNested(pieceType, piece);
20
+ }
21
+ }
22
+ if(selection.type === "personal"){
23
+ for(const pieces of Object.values(selection.value)){
24
+ for(const piece of pieces){
25
+ downloadTracker.tryToDownloadAndNested(pieceType, piece);
26
+ }
27
+ }
28
+ }
29
+ }
30
+ const results = await Promise.all(downloadTracker.getDownloadPromises());
31
+ progressHandlers.onProgress({ type: ROSTERLOCK_DOWNLOAD_STATE.downloadAllComplete });
32
+ return PieceDownloadTracker.resultsToMap(results);
33
+ }catch(e){
34
+ progressHandlers.onProgress({
35
+ type: ROSTERLOCK_DOWNLOAD_STATE.downloadFullFailure,
36
+ error: (e as Error).message,
37
+ });
38
+ throw e;
39
+ }
40
+ }
41
+
@@ -0,0 +1,9 @@
1
+
2
+ import { RosterLockDownloadUpdate } from "@roster-lock/types";
3
+
4
+ export type ProgressHandlers = {
5
+ onProgress: (update: RosterLockDownloadUpdate) => void;
6
+ abortSignal: AbortSignal;
7
+ }
8
+
9
+ export { DownloadResult, DownloadResultsMap } from "@roster-lock/types";
@@ -0,0 +1,29 @@
1
+
2
+ import { JSON_Unknown, ENCRYPT_SYMMETRIC, SymmetricEncryptionKey } from "@roster-lock/utils";
3
+
4
+ import { JSONSchemaType } from "ajv";
5
+
6
+ export const encryptedSchema: JSONSchemaType<string> = {
7
+ type: "string",
8
+ };
9
+
10
+
11
+ export async function encryptJSON(
12
+ originalValue: JSON_Unknown
13
+ ): Promise<{ key: SymmetricEncryptionKey, encryptedValue: string }> {
14
+ const key = await ENCRYPT_SYMMETRIC.generateKey();
15
+ const encryptedValue = await ENCRYPT_SYMMETRIC.encryptValue(key, originalValue);
16
+ return {
17
+ key: key,
18
+ encryptedValue: encryptedValue
19
+ };
20
+ }
21
+
22
+
23
+ export async function decryptJSON(
24
+ keyString: string, encryptedValue: string
25
+ ): Promise<JSON_Unknown> {
26
+ return await ENCRYPT_SYMMETRIC.decryptValue(
27
+ keyString as SymmetricEncryptionKey, encryptedValue
28
+ ) as JSON_Unknown;
29
+ }
@@ -0,0 +1,9 @@
1
+
2
+ export function createRandomSeed(){
3
+ const array = new Uint8Array(16); // 16 bytes = 128 bits = 32 hex characters
4
+ crypto.getRandomValues(array);
5
+ const randHex = Array.from(array)
6
+ .map(b => b.toString(16).padStart(2, "0"))
7
+ .join(""); // 32-character hex string
8
+ return randHex;
9
+ }
@@ -0,0 +1,33 @@
1
+ import { HTTPRouter } from "../../utils/http-router";
2
+ import { WebSocketRouter } from "../../utils/websocket-router";
3
+
4
+ import { wsHandler } from "./room-handler-bridge/ws-handler";
5
+ import { httpHandler } from "./room-handler-bridge/http-handler";
6
+
7
+ import { getFilesOfAsset, getPieceFileContents } from "./file-routes";
8
+ import { ensurePieceDownloaded, ensurePieceDownloadedWs, listDownloadedPiecesDirect } from "./select";
9
+ import { listAvailableSortPlugins, sortListPlugin, gameComplete } from "./piece-sort";
10
+ import { IFolderDB, V1Env } from "./globals";
11
+ import { PluginManager } from "@roster-lock/plugin-runtime";
12
+
13
+ export const createV1Routers = (fileDB: IFolderDB, pluginRuntime: PluginManager)=>{
14
+ const env: V1Env = { fileDB, pluginRuntime };
15
+ const httpRouter = new HTTPRouter();
16
+ const wsRouter = new WebSocketRouter()
17
+
18
+ httpRouter.post("/sync-dl", httpHandler.bind(env));
19
+ httpRouter.put("/piece", ensurePieceDownloaded.bind(env))
20
+ httpRouter.post("/piece/asset-files", getFilesOfAsset.bind(env));
21
+ httpRouter.post("/piece/file-contents", getPieceFileContents.bind(env));
22
+ httpRouter.query("/piece/list-downloaded/direct", listDownloadedPiecesDirect.bind(env));
23
+ httpRouter.get("/piece/sort-list/available", listAvailableSortPlugins.bind(env));
24
+ httpRouter.post("/piece/sort-list/plugin/:pluginName", sortListPlugin.bind(env));
25
+ httpRouter.post("/game-complete", gameComplete.bind(env));
26
+
27
+
28
+ wsRouter.mount("/sync-dl", wsHandler.bind(env));
29
+ wsRouter.mount("/piece/ensure", ensurePieceDownloadedWs.bind(env));
30
+
31
+ return { httpRouter, wsRouter };
32
+ }
33
+
@@ -0,0 +1,96 @@
1
+
2
+ import { RosterLockV1Config, UserSelection } from "@roster-lock/types";
3
+ import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
4
+ import { jsonBody, HTTPRequestHandler, HTTPError } from "../../utils/http-router";
5
+ import { UserSelectionSchema } from "./schema/selected";
6
+ import { V1Env } from "./globals/types";
7
+ import z, { ZodType } from "zod";
8
+
9
+ function castLockConfig(uncasted: unknown): RosterLockV1Config {
10
+ const casted = ROSTERLOCK_V1_CASTER_JSONSCHEMA.safeCast(uncasted, true);
11
+ if(!casted.valid) throw new HTTPError(400, "Invalid lockConfig", casted.error);
12
+ return casted.value;
13
+ }
14
+
15
+ // The plugin only knows how to rank pieces it has stats for - it may return a
16
+ // partial ranking (or none at all). This reorders the caller's own candidate
17
+ // list to match that ranking, appending anything the plugin didn't rank (in
18
+ // its original order) at the end, so the response always covers every id the
19
+ // caller asked about.
20
+ function reorderByRanking(candidateIds: Array<string>, ranked: Array<string>): Array<string> {
21
+ const candidateSet = new Set(candidateIds);
22
+ const rankedFiltered = ranked.filter((id)=>candidateSet.has(id));
23
+ const rankedSet = new Set(rankedFiltered);
24
+ const leftover = candidateIds.filter((id)=>!rankedSet.has(id));
25
+ return [...rankedFiltered, ...leftover];
26
+ }
27
+
28
+ export const listAvailableSortPlugins: HTTPRequestHandler = async function(
29
+ this: V1Env, { res }
30
+ ){
31
+ const available = await this.pluginRuntime.pieceSort.listAvailable();
32
+ res.writeHead(200, { "Content-Type": "application/json" });
33
+ res.end(JSON.stringify(available));
34
+ }
35
+
36
+ type SortListBody = {
37
+ lockConfig: unknown,
38
+ pieceType: string,
39
+ logicIds: Array<string>,
40
+ }
41
+
42
+ const sortListBodySchema: ZodType<SortListBody> = z.object({
43
+ lockConfig: z.unknown(),
44
+ pieceType: z.string(),
45
+ logicIds: z.array(z.string()),
46
+ }).strict();
47
+
48
+ export const sortListPlugin: HTTPRequestHandler = async function(
49
+ this: V1Env, { req, res }, routeInfo
50
+ ){
51
+ const pluginName = routeInfo.params.pluginName;
52
+ if(!pluginName) throw new HTTPError(400, "Missing pluginName");
53
+
54
+ const body = await jsonBody(req);
55
+ const parseResult = sortListBodySchema.safeParse(body);
56
+ if(!parseResult.success) throw new HTTPError(400, "Bad Form", parseResult.error);
57
+ const { pieceType, logicIds } = parseResult.data;
58
+ const lockConfig = castLockConfig(parseResult.data.lockConfig);
59
+
60
+ const ranked = await this.pluginRuntime.pieceSort.sortPieces(pluginName, { lockConfig, pieceType });
61
+ const result = reorderByRanking(logicIds, ranked);
62
+
63
+ res.writeHead(200, { "Content-Type": "application/json" });
64
+ res.end(JSON.stringify(result));
65
+ }
66
+
67
+ type GameCompleteBody = {
68
+ lockConfig: unknown,
69
+ localUsers: Array<string>,
70
+ userSelections: Record<string, UserSelection>,
71
+ winners: Array<string>,
72
+ }
73
+
74
+ const gameCompleteBodySchema: ZodType<GameCompleteBody> = z.object({
75
+ lockConfig: z.unknown(),
76
+ localUsers: z.array(z.string()),
77
+ userSelections: z.record(z.string(), UserSelectionSchema),
78
+ winners: z.array(z.string()),
79
+ }).strict();
80
+
81
+ export const gameComplete: HTTPRequestHandler = async function(
82
+ this: V1Env, { req, res }
83
+ ){
84
+ const body = await jsonBody(req);
85
+ const parseResult = gameCompleteBodySchema.safeParse(body);
86
+ if(!parseResult.success) throw new HTTPError(400, "Bad Form", parseResult.error);
87
+ const { localUsers, userSelections, winners } = parseResult.data;
88
+ const lockConfig = castLockConfig(parseResult.data.lockConfig);
89
+
90
+ await this.pluginRuntime.pieceSort.handleGameComplete({
91
+ lockConfig, localUsers, userSelections, winners,
92
+ });
93
+
94
+ res.writeHead(200, { "Content-Type": "application/json" });
95
+ res.end(JSON.stringify({ ok: true }));
96
+ }
@@ -0,0 +1,125 @@
1
+
2
+ import { WebSocket } from "ws";
3
+ import { MessageBridge, handleFetch } from "@roster-lock/utils";
4
+ import { bindStepsToBridge, ProgressListeners } from "./steps";
5
+ import { RosterLockV1Config, RosterLockV1SyncDLRequestClientToAgent } from "@roster-lock/types";
6
+ import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
7
+ import { UserSelectionSchema } from "../schema/selected";
8
+ import { IFolderDB } from "../globals/FolderDB";
9
+ import { once } from "events";
10
+ import z, { ZodType } from "zod";
11
+ import { PluginManager } from "@roster-lock/plugin-runtime";
12
+
13
+ const RequestCaster: ZodType<(
14
+ & RosterLockV1SyncDLRequestClientToAgent
15
+ & { rosterConfig: any }
16
+ )> = z.object({
17
+ relay: z.object({
18
+ url: z.string(),
19
+ roomId: z.string(),
20
+ }),
21
+ machine: z.object({
22
+ timestamp: z.number(),
23
+ publicKey: z.string(),
24
+ signature: z.string(),
25
+ }),
26
+ rosterConfig: z.any(),
27
+ // Record<number, UserSelection> at the type level, but JSON object keys
28
+ // are always strings on the wire - validate them as such.
29
+ playerSelections: z.record(z.string(), UserSelectionSchema),
30
+ }).strict();
31
+
32
+ export function castRoomRequest(uncasted: unknown): RosterLockV1SyncDLRequestClientToAgent {
33
+ const parsed = RequestCaster.safeParse(uncasted);
34
+ if(!parsed.success) throw new Error("Invalid request");
35
+ const roomRequest = parsed.data;
36
+ const config = ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(roomRequest.rosterConfig, true);
37
+ const casted = {
38
+ ...roomRequest,
39
+ rosterConfig: config,
40
+ };
41
+ return casted;
42
+ }
43
+
44
+ type Machine = {
45
+ userId: string,
46
+ publicKey: string,
47
+ displayName: string,
48
+ playerCount: number,
49
+ };
50
+
51
+ export async function exchangeAndDownloadSelections(
52
+ fileDB: IFolderDB,
53
+ pluginRuntime: PluginManager,
54
+ roomRequest: RosterLockV1SyncDLRequestClientToAgent,
55
+ progressListeners: ProgressListeners = {}
56
+ ){
57
+ const roomURL = prepareRelayURL(roomRequest);
58
+
59
+ const httpURL = new URL(roomURL);
60
+ httpURL.pathname = "/api/v1/room/" + roomRequest.relay.roomId + "/machines";
61
+ const machines = await handleFetch<Array<Machine>>(fetch(httpURL.href));
62
+
63
+ const wsURL = new URL(roomURL);
64
+ wsURL.protocol = roomURL.protocol === "https:" ? "wss:" : "ws:";
65
+ wsURL.pathname = "/api/v1/room/" + roomRequest.relay.roomId;
66
+ // perMessageDeflate disabled: workerd's hibernatable WebSocket (relay-server,
67
+ // local wrangler dev) mishandles fragmented/compressed frames for larger
68
+ // messages, corrupting frame parsing on the "ws" client (surfaces as
69
+ // "Invalid WebSocket frame: invalid payload length").
70
+ const roomWebSocket = new WebSocket(wsURL.href, { perMessageDeflate: false });
71
+ const roomBridge = new MessageBridge((message)=>roomWebSocket.send(JSON.stringify(message)));
72
+ roomWebSocket.on("message", (message)=>{
73
+ roomBridge.handleMessage(JSON.parse(message.toString()));
74
+ });
75
+ roomWebSocket.on("error", (err)=>{
76
+ // An unhandled "error" event on an EventEmitter throws and crashes the
77
+ // whole process - and one match-agent process serves many concurrent
78
+ // games, so a single room's socket hiccup must not take the rest down.
79
+ console.error("room websocket error", err);
80
+ });
81
+
82
+ const roomPromise = bindStepsToBridge({
83
+ bridge: roomBridge,
84
+ fileDB,
85
+ pluginRuntime,
86
+ machines: machines.map(machine=>({ publicKey: machine.publicKey, playerCount: machine.playerCount })),
87
+ ownMachinePublicKey: roomRequest.machine.publicKey,
88
+ ownSelections: roomRequest.playerSelections,
89
+ lockConfig: roomRequest.rosterConfig,
90
+ gameControlledSelections: {},
91
+ }, progressListeners);
92
+
93
+ // The relay closes the room websocket with a close reason as soon as it fails
94
+ // a room (e.g. a download failure on the other user's leg), but never sends a
95
+ // bridge-level "error" event - so without this, bindStepsToBridge's onEvent("error")
96
+ // handler never fires and the room hangs until its own 30s heartbeat times out,
97
+ // surfacing a misleading "Heartbeat Timeout" instead of the real failure reason.
98
+ const closedEarly = new Promise<never>((_, reject)=>{
99
+ roomWebSocket.on("close", (code, reason)=>{
100
+ const reasonText = reason.toString();
101
+ reject(new Error(reasonText || `Room websocket closed unexpectedly (code ${code})`));
102
+ });
103
+ });
104
+ // roomPromise usually wins the race (success or a bridge-level "error" event),
105
+ // in which case this rejection is never observed - swallow it here so that
106
+ // doesn't crash the process as an unhandled rejection.
107
+ closedEarly.catch(()=>{});
108
+
109
+ try {
110
+ await once(roomWebSocket, 'open')
111
+ return await Promise.race([roomPromise, closedEarly]);
112
+ } finally {
113
+ roomWebSocket.close();
114
+ }
115
+ }
116
+
117
+ function prepareRelayURL({ relay, machine }: RosterLockV1SyncDLRequestClientToAgent){
118
+ const url = new URL(relay.url);
119
+ url.searchParams.set("room", relay.roomId);
120
+ url.searchParams.set("t", machine.timestamp.toString());
121
+ url.searchParams.set("pk", machine.publicKey);
122
+ url.searchParams.set("sig", machine.signature);
123
+ return url;
124
+ }
125
+
@@ -0,0 +1,16 @@
1
+
2
+ import { jsonBody, HTTPRequestHandler } from "../../../utils/http-router";
3
+ import { castRoomRequest, exchangeAndDownloadSelections } from "./exchange-and-download-selections";
4
+ import { V1Env } from "../globals/types";
5
+
6
+ export const httpHandler: HTTPRequestHandler = async function(this: V1Env, { req, res }, params, next){
7
+ try {
8
+ const body = await jsonBody(req);
9
+ const roomRequest = castRoomRequest(body);
10
+ const results = await exchangeAndDownloadSelections(this.fileDB, this.pluginRuntime, roomRequest);
11
+ res.writeHead(200, { "Content-Type": "application/json" });
12
+ res.end(JSON.stringify(results));
13
+ }catch(e){
14
+ next(e);
15
+ }
16
+ }