diff --git a/package-lock.json b/package-lock.json index fdae3f6..b8c464d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@solidjs/router": "^0.15.0", "@thisbeyond/solid-dnd": "^0.7.5", "@types/three": "^0.183.1", + "aedes": "^1.1.1", "chokidar": "^5.0.0", "commander": "^14.0.3", "csv-parse": "^6.1.0", @@ -23,11 +24,13 @@ "marked-directive": "^1.0.7", "marked-gfm-heading-id": "^4.1.3", "mermaid": "^11.0.0", + "mqtt": "^5.15.1", "solid-element": "^1.9.1", "solid-js": "^1.9.3", "three": "^0.183.2", "three-3mf-exporter": "^45.1.0", - "yarn-spinner-loader": "^0.1.0" + "yarn-spinner-loader": "^0.1.0", + "zod": "^4.4.3" }, "bin": { "ttrpg": "dist/cli/index.js" @@ -721,6 +724,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1871,6 +1883,15 @@ "zod": "^3.23.8" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@module-federation/error-codes": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", @@ -3310,12 +3331,20 @@ "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", - "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3364,6 +3393,15 @@ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -3405,6 +3443,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -3441,6 +3491,138 @@ "node": ">=0.4.0" } }, + "node_modules/aedes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aedes/-/aedes-1.1.1.tgz", + "integrity": "sha512-kmKFRgQrjGyv9HuVqpNGM/aPD2K4bf20aF1UbrxEvh3j8BJ1OD0rHi3E3u3xfrgc3ydj2+9UiPdW0sHiP6d+Sg==", + "license": "MIT", + "dependencies": { + "aedes-packet": "^3.0.0", + "aedes-persistence": "^10.2.2", + "mqemitter": "^7.1.0", + "mqtt-packet": "^9.0.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/aedes" + }, + "peerDependencies": { + "aedes-persistence-level": "^9.1.2", + "aedes-persistence-mongodb": "^9.3.1", + "aedes-persistence-redis": "^11.2.1" + }, + "peerDependenciesMeta": { + "aedes-persistence-level": { + "optional": true + }, + "aedes-persistence-mongodb": { + "optional": true + }, + "aedes-persistence-redis": { + "optional": true + } + } + }, + "node_modules/aedes-packet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz", + "integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==", + "license": "MIT", + "dependencies": { + "mqtt-packet": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/aedes-packet/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/aedes-packet/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/aedes-packet/node_modules/mqtt-packet": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz", + "integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/aedes-packet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/aedes-persistence": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-10.3.1.tgz", + "integrity": "sha512-C/z0zLEXY9PkUH+7pz31H5gGzlQZxqhFONK1lt5VON0hiHXQq6tr4cSIAtFUVQ1kyUdd7SsfkeKLAECp3gjjpw==", + "license": "MIT", + "dependencies": { + "aedes-packet": "^4.0.1", + "qlobber": "^8.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/aedes-persistence/node_modules/aedes-packet": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-4.0.1.tgz", + "integrity": "sha512-PLjKJElXEYB66RzHhJCw0SrRLfSmGRbbg2mtO5t/6FAbFp8vC0cG5ErhvPvfa9iKBrKZwoluWLgn8doBU8sRAw==", + "license": "MIT", + "dependencies": { + "mqtt-packet": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3727,6 +3909,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.15", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz", @@ -3740,6 +3942,63 @@ "node": ">=6.0.0" } }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/bl/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", @@ -3763,6 +4022,18 @@ "node": ">=8" } }, + "node_modules/broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -3820,11 +4091,34 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, "license": "MIT" }, "node_modules/bytes": { @@ -4063,6 +4357,12 @@ "node": ">=20" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, "node_modules/component-register": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/component-register/-/component-register-0.8.8.tgz", @@ -4076,6 +4376,35 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", @@ -4778,7 +5107,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5119,6 +5447,24 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5199,6 +5545,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -5250,6 +5609,16 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -5571,6 +5940,12 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -5685,6 +6060,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -5748,6 +6143,15 @@ "node": ">=12" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7430,6 +7834,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8167,7 +8581,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8191,11 +8604,117 @@ "integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==", "license": "BSD-3-Clause" }, + "node_modules/mqemitter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-7.1.0.tgz", + "integrity": "sha512-GnBDNz3lxmllW201ne0mrmdy5tPOTnc79jjVcsfUa2LG2pUGeyGWVeiae6ZysfC/64XrYOqCKRAQYrB7pGyBVQ==", + "license": "ISC", + "dependencies": { + "fastparallel": "^2.4.1", + "qlobber": "^8.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/mqtt": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz", + "integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/mqtt/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/mqtt/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -8268,6 +8787,16 @@ "node": ">=8" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/nwsapi": { "version": "2.2.23", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", @@ -8624,6 +9153,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8684,6 +9222,15 @@ ], "license": "MIT" }, + "node_modules/qlobber": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz", + "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -8874,6 +9421,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -9071,6 +9624,30 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/solid-element": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/solid-element/-/solid-element-1.9.1.tgz", @@ -9139,6 +9716,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -9611,7 +10197,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "devOptional": true, "license": "0BSD" }, "node_modules/type-detect": { @@ -9637,6 +10222,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -9675,7 +10266,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -10016,6 +10606,53 @@ "dev": true, "license": "MIT" }, + "node_modules/worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.33", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.33.tgz", + "integrity": "sha512-RQVlKkek80v8M6SHvdMKiywaXFmX3XTpYTXTw0r62PdXB06t74XNxcTuG8wsQwnjorAQymNQyyQ0rzv7PykEMQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10059,7 +10696,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -10094,6 +10730,15 @@ "dev": true, "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -10194,9 +10839,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 6ec9ec5..838b46e 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@solidjs/router": "^0.15.0", "@thisbeyond/solid-dnd": "^0.7.5", "@types/three": "^0.183.1", + "aedes": "^1.1.1", "chokidar": "^5.0.0", "commander": "^14.0.3", "csv-parse": "^6.1.0", @@ -46,11 +47,13 @@ "marked-directive": "^1.0.7", "marked-gfm-heading-id": "^4.1.3", "mermaid": "^11.0.0", + "mqtt": "^5.15.1", "solid-element": "^1.9.1", "solid-js": "^1.9.3", "three": "^0.183.2", "three-3mf-exporter": "^45.1.0", - "yarn-spinner-loader": "^0.1.0" + "yarn-spinner-loader": "^0.1.0", + "zod": "^4.4.3" }, "devDependencies": { "@image-tracer-ts/core": "^1.0.2", diff --git a/src/cli/commands/serve.ts b/src/cli/commands/serve.ts index d3d7b20..86adb80 100644 --- a/src/cli/commands/serve.ts +++ b/src/cli/commands/serve.ts @@ -5,6 +5,7 @@ import { createReadStream } from "fs"; import { join, resolve, extname, sep, relative, dirname } from "path"; import { watch } from "chokidar"; import { fileURLToPath } from "url"; +import { createJournalServer } from "../journal.js"; interface ContentIndex { [path: string]: string; @@ -63,7 +64,11 @@ export function scanDirectory(dir: string): ContentIndex { const stats = statSync(fullPath); if (stats.isDirectory()) { scan(fullPath, relPath); - } else if (entry.endsWith(".md") || entry.endsWith(".csv") || entry.endsWith(".yarn")) { + } else if ( + entry.endsWith(".md") || + entry.endsWith(".csv") || + entry.endsWith(".yarn") + ) { try { const content = readFileSync(fullPath, "utf-8"); index[normalizedRelPath] = content; @@ -205,6 +210,7 @@ export function createContentServer( port: number, distPath: string = distDir, host: string = "0.0.0.0", + mqttPort: number = 1883, ): ContentServer { let contentIndex: ContentIndex = {}; @@ -216,14 +222,18 @@ export function createContentServer( // 监听文件变化 console.log("监听文件变化..."); const watcher = watch(contentDir, { - ignored: /(^|[\/\\])\../, + ignored: /(^|[\/\\])\./, persistent: true, ignoreInitial: true, }); watcher .on("add", (path) => { - if (path.endsWith(".md") || path.endsWith(".csv") || path.endsWith(".yarn")) { + if ( + path.endsWith(".md") || + path.endsWith(".csv") || + path.endsWith(".yarn") + ) { try { const content = readFileSync(path, "utf-8"); const relPath = "/" + relative(contentDir, path).split(sep).join("/"); @@ -235,7 +245,11 @@ export function createContentServer( } }) .on("change", (path) => { - if (path.endsWith(".md") || path.endsWith(".csv") || path.endsWith(".yarn")) { + if ( + path.endsWith(".md") || + path.endsWith(".csv") || + path.endsWith(".yarn") + ) { try { const content = readFileSync(path, "utf-8"); const relPath = "/" + relative(contentDir, path).split(sep).join("/"); @@ -247,13 +261,24 @@ export function createContentServer( } }) .on("unlink", (path) => { - if (path.endsWith(".md") || path.endsWith(".csv") || path.endsWith(".yarn")) { + if ( + path.endsWith(".md") || + path.endsWith(".csv") || + path.endsWith(".yarn") + ) { const relPath = "/" + relative(contentDir, path).split(sep).join("/"); delete contentIndex[relPath]; console.log(`[删除] ${relPath}`); } }); + // ---- Journal / MQTT broker ---- + let journal: Awaited> | null = null; + + createJournalServer(contentDir, mqttPort).then((j) => { + journal = j; + }); + // 创建请求处理器 const handleRequest = createRequestHandler( contentDir, @@ -269,7 +294,9 @@ export function createContentServer( console.log(`\n开发服务器已启动:http://${displayHost}:${port}`); console.log(`内容目录:${contentDir}`); console.log(`静态资源目录:${distPath}`); - console.log(`索引文件:http://${displayHost}:${port}/__CONTENT_INDEX.json\n`); + console.log( + `MQTT 代理端口:${mqttPort} (connect via tcp://${displayHost}:${mqttPort})\n`, + ); }); return { @@ -280,6 +307,7 @@ export function createContentServer( console.log("关闭内容服务器..."); server.close(); watcher.close(); + if (journal) journal.close(); }, }; } @@ -291,6 +319,7 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => { const contentDir = resolve(dir); const port = parseInt(options.port, 10); const host = options.host || "0.0.0.0"; + const mqttPort = parseInt(options.mqttPort || "1883", 10); - createContentServer(contentDir, port, distDir, host); + createContentServer(contentDir, port, distDir, host, mqttPort); }; diff --git a/src/cli/index.ts b/src/cli/index.ts index 4f10856..86f9a3d 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,33 +1,33 @@ #!/usr/bin/env node -import { Command } from 'commander'; -import { serveCommand } from './commands/serve.js'; -import { compileCommand } from './commands/compile.js'; -import { mcpCommand } from './commands/mcp.js'; -import type { ServeOptions, CompileOptions } from './types.js'; +import { Command } from "commander"; +import { serveCommand } from "./commands/serve.js"; +import { compileCommand } from "./commands/compile.js"; +import { mcpCommand } from "./commands/mcp.js"; +import type { ServeOptions, CompileOptions } from "./types.js"; const program = new Command(); program - .name('ttrpg') - .description('TTRPG 工具箱 - 用于编译和预览 TTRPG 文档') - .version('0.0.1'); + .name("ttrpg") + .description("TTRPG 工具箱 - 用于编译和预览 TTRPG 文档") + .version("0.0.1"); program - .command('serve') - .description('运行一个 web 服务器预览目录中的内容,并实时监听更新') - .argument('[dir]', '要预览的目录', '.') - .option('-p, --port ', '端口号', '3000') - .option('-h, --host ', '主机地址', '0.0.0.0') + .command("serve") + .description("运行一个 web 服务器预览目录中的内容,并实时监听更新") + .argument("[dir]", "要预览的目录", ".") + .option("-p, --port ", "HTTP 端口号", "3000") + .option("--mqtt-port ", "MQTT 代理端口号", "1883") + .option("-h, --host ", "主机地址", "0.0.0.0") .action(serveCommand); program - .command('compile') - .description('将目录中的内容输出为带 hash 路由、单个 html 入口的 web 应用') - .argument('[dir]', '要编译的目录', '.') - .option('-o, --output ', '输出目录', './dist/output') + .command("compile") + .description("将目录中的内容输出为带 hash 路由、单个 html 入口的 web 应用") + .argument("[dir]", "要编译的目录", ".") + .option("-o, --output ", "输出目录", "./dist/output") .action(compileCommand); -program - .addCommand(mcpCommand); +program.addCommand(mcpCommand); program.parse(); diff --git a/src/cli/journal.ts b/src/cli/journal.ts new file mode 100644 index 0000000..516f361 --- /dev/null +++ b/src/cli/journal.ts @@ -0,0 +1,227 @@ +/** + * Journal stream persistence — MQTT broker + JSONL append + session manifest + * + * Starts an aedes MQTT broker and connects to itself to: + * 1. Persist all stream messages to .ttrpg/sessions/{id}/stream.jsonl + * 2. Manage session lifecycle via retained meta topics + * + * No HTTP routes — the JSONL files are served by the existing static server. + */ + +import { join, resolve } from "path"; +import { + appendFileSync, + existsSync, + mkdirSync, + readFileSync, + rmdirSync, + unlinkSync, + writeFileSync, +} from "fs"; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface SessionMeta { + name: string; + created: number; + players: string[]; +} + +interface SessionManifest { + sessions: Record; +} + +export interface JournalServer { + /** MQTT broker instance */ + broker: import("aedes").Aedes; + /** TCP server the broker is attached to */ + tcpServer: import("net").Server; + /** Persistence client (server subscribes to itself) */ + persistenceClient: import("mqtt").MqttClient | null; + /** Clean shutdown */ + close(): Promise; +} + +// --------------------------------------------------------------------------- +// Path helpers +// --------------------------------------------------------------------------- + +function dataDir(contentDir: string): string { + const d = resolve(contentDir, ".ttrpg"); + mkdirSync(d, { recursive: true }); + return d; +} + +function manifestPath(dataRoot: string): string { + return join(dataRoot, "manifest.json"); +} + +function sessionDir(dataRoot: string, id: string): string { + return join(dataRoot, "sessions", id); +} + +function streamPath(dataRoot: string, id: string): string { + return join(sessionDir(dataRoot, id), "stream.jsonl"); +} + +// --------------------------------------------------------------------------- +// Manifest +// --------------------------------------------------------------------------- + +function loadManifest(dataRoot: string): SessionManifest { + const p = manifestPath(dataRoot); + if (!existsSync(p)) return { sessions: {} }; + try { + return JSON.parse(readFileSync(p, "utf-8")); + } catch { + return { sessions: {} }; + } +} + +function saveManifest(dataRoot: string, m: SessionManifest): void { + writeFileSync(manifestPath(dataRoot), JSON.stringify(m, null, 2), "utf-8"); +} + +// --------------------------------------------------------------------------- +// JSONL persistence +// --------------------------------------------------------------------------- + +function appendStream(dataRoot: string, id: string, line: string): void { + const sd = sessionDir(dataRoot, id); + mkdirSync(sd, { recursive: true }); + appendFileSync(streamPath(dataRoot, id), line + "\n", "utf-8"); +} + +// --------------------------------------------------------------------------- +// Server factory +// --------------------------------------------------------------------------- + +export async function createJournalServer( + contentDir: string, + mqttPort: number, +): Promise { + const root = dataDir(contentDir); + console.log(`[journal] data dir: ${root}`); + + // ---- MQTT Broker ---- + const { Aedes: AedesFactory } = await import("aedes"); + const { createServer } = await import("net"); + + const broker = new AedesFactory(); + const tcpServer = createServer(broker.handle.bind(broker)); + + tcpServer.listen(mqttPort, "0.0.0.0", () => { + console.log(`[journal] MQTT broker on port ${mqttPort}`); + }); + + // ---- Persistence + Manifest client ---- + let client: import("mqtt").MqttClient | null = null; + + const { default: mqtt } = await import("mqtt"); + client = mqtt.connect(`mqtt://127.0.0.1:${mqttPort}`, { + clientId: "journal-server", + }); + + client.on("connect", () => { + // 1. Persistence: append every stream message to JSONL + client!.subscribe("ttrpg/+/stream", { qos: 1 }, (err) => { + if (err) console.error("[journal] stream sub err:", err); + }); + + // 2. Session meta: manage manifest from retained meta topics + client!.subscribe("ttrpg/+/meta", { qos: 1 }, (err) => { + if (err) console.error("[journal] meta sub err:", err); + }); + }); + + client.on("message", (topic, payload) => { + const parts = topic.split("/"); + if (parts.length < 3) return; + const sessionId = parts[1]; + const subtopic = parts[2]; + + if (subtopic === "stream") { + // Persist every stream message + try { + appendStream(root, sessionId, payload.toString()); + } catch (e) { + console.error(`[journal] stream append err for ${sessionId}:`, e); + } + } else if (subtopic === "meta") { + // Session lifecycle + handleMetaChange(root, sessionId, client!, payload.toString()); + } + }); + + console.log("[journal] persistence listener active"); + + return { + broker, + tcpServer, + persistenceClient: client, + async close() { + console.log("[journal] shutting down..."); + client?.end(true); + await new Promise((r) => broker.close(() => r())); + await new Promise((r) => tcpServer.close(() => r())); + }, + }; +} + +// --------------------------------------------------------------------------- +// Session lifecycle +// --------------------------------------------------------------------------- + +const $SESSIONS = "ttrpg/$SESSIONS"; + +function handleMetaChange( + root: string, + sessionId: string, + client: import("mqtt").MqttClient, + rawPayload: string, +): void { + const manifest = loadManifest(root); + + try { + const meta = JSON.parse(rawPayload) as SessionMeta | null; + + if (!meta || !meta.name) { + // Tombstone: empty or invalid payload → delete session + if (manifest.sessions[sessionId]) { + delete manifest.sessions[sessionId]; + + // Wipe files + const sp = streamPath(root, sessionId); + if (existsSync(sp)) unlinkSync(sp); + const sd = sessionDir(root, sessionId); + try { + rmdirSync(sd); + } catch { + /* not empty, fine */ + } + + console.log(`[journal] session deleted: ${sessionId}`); + } + } else { + // Create or update + manifest.sessions[sessionId] = { + name: meta.name, + created: meta.created || Date.now(), + players: meta.players || [], + }; + console.log(`[journal] session updated: ${sessionId} (${meta.name})`); + } + + saveManifest(root, manifest); + + // Republish full manifest as a retained message + client.publish($SESSIONS, JSON.stringify(manifest), { + qos: 1, + retain: true, + }); + } catch (e) { + console.error(`[journal] meta parse err for ${sessionId}:`, e); + } +} diff --git a/src/cli/types.ts b/src/cli/types.ts index f323a8b..29875ad 100644 --- a/src/cli/types.ts +++ b/src/cli/types.ts @@ -1,14 +1,21 @@ export interface ServeOptions { port: string; host?: string; + mqttPort?: string; } export interface CompileOptions { output: string; } -export type ServeCommandHandler = (dir: string, options: ServeOptions) => Promise; -export type CompileCommandHandler = (dir: string, options: CompileOptions) => Promise; +export type ServeCommandHandler = ( + dir: string, + options: ServeOptions, +) => Promise; +export type CompileCommandHandler = ( + dir: string, + options: CompileOptions, +) => Promise; export interface MarkdownFile { path: string;