@utsp/runtime-server 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Thomas Piquet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @utsp/runtime-server
2
+
3
+ > ⚠️ **PROTOTYPE - NOT READY FOR PRODUCTION**
4
+ >
5
+ > This package is currently in early development and should **NOT** be used in production.
6
+ > The API is unstable and subject to breaking changes without notice.
7
+
8
+ Server-side runtime for UTSP (Universal Text Stream Protocol) multi-user applications.
9
+
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ ## ⚠️ Development Status
13
+
14
+ **This is a prototype package under active development.**
15
+
16
+ - ❌ No stable API
17
+ - ❌ No documentation available yet
18
+ - ❌ Breaking changes expected
19
+ - ❌ Not recommended for production use
20
+
21
+ **Please check back later for updates or watch the repository for release announcements.**
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @utsp/runtime-server
27
+ ```
28
+
29
+ ## Repository
30
+
31
+ - [GitHub](https://github.com/thp-software/utsp)
32
+ - [Issues](https://github.com/thp-software/utsp/issues)
33
+
34
+ ## License
35
+
36
+ MIT © 2025 Thomas Piquet
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var h=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var f=(n,t,e)=>t in n?h(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var l=(n,t)=>h(n,"name",{value:t,configurable:!0});var w=(n,t)=>{for(var e in t)h(n,e,{get:t[e],enumerable:!0})},T=(n,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of d(t))!k.call(n,s)&&s!==e&&h(n,s,{get:()=>t[s],enumerable:!(i=v(t,s))||i.enumerable});return n};var S=n=>T(h({},"__esModule",{value:!0}),n);var r=(n,t,e)=>(f(n,typeof t!="symbol"?t+"":t,e),e);var y={};w(y,{ServerRuntime:()=>p});module.exports=S(y);var u=require("@utsp/core"),g=require("@utsp/network-server");var c=class c{constructor(t){r(this,"core");r(this,"network");r(this,"options");r(this,"running",!1);r(this,"startTime",0);r(this,"lastTimestamp",0);r(this,"tickCount",0);r(this,"tps",0);r(this,"tpsUpdateTime",0);r(this,"tickInterval",null);this.options={application:t.application,port:t.port,host:t.host??"0.0.0.0",tickRate:t.tickRate??20,maxConnections:t.maxConnections??100,debug:t.debug??!1,width:t.width??80,height:t.height??25,cors:t.cors??{origin:"*"}},this.log("Initializing ServerRuntime..."),this.core=new u.Core({mode:"server",maxUsers:this.options.maxConnections}),this.network=new g.SocketIOServer({port:this.options.port,host:this.options.host,cors:this.options.cors,maxConnections:this.options.maxConnections,debug:this.options.debug}),this.log("ServerRuntime initialized")}getMode(){return"server"}isRunning(){return this.running}setTickRate(t){if(t<=0||t>1e3)throw new Error(`Invalid tick rate: ${t}. Must be between 1 and 1000.`);if(this.options.tickRate=t,this.log(`Tick rate changed to ${t} TPS`),this.running&&this.tickInterval){clearInterval(this.tickInterval);let e=1e3/t;this.log(`Restarting tick loop at ${t} TPS (${e}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},e)}}getTickRate(){return this.options.tickRate}async start(){if(this.running){this.log("Already running");return}this.log("Starting ServerRuntime..."),await this.network.start(),this.log(`Server listening on ${this.options.host}:${this.options.port}`),this.setupNetworkHandlers(),this.log("Calling application.init()..."),this.options.application.init(this.core,this),this.running=!0,this.startTime=Date.now(),this.lastTimestamp=this.startTime,this.tpsUpdateTime=this.startTime;let t=1e3/this.options.tickRate;this.log(`Starting tick loop at ${this.options.tickRate} TPS (${t}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},t)}async stop(){if(!this.running)return;this.log("Stopping ServerRuntime..."),this.running=!1,this.tickInterval&&(clearInterval(this.tickInterval),this.tickInterval=null),this.network.getClients().forEach(e=>{this.network.disconnectClient(e,"Server stopped")}),await this.network.stop(),this.log("ServerRuntime stopped")}getStats(){return{mode:"server",running:this.running,userCount:this.core.getUsers().length,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0,totalTicks:this.tickCount}}async destroy(){await this.stop(),this.log("Destroying ServerRuntime..."),this.options.application.destroy&&this.options.application.destroy(),await this.network.destroy(),this.log("ServerRuntime destroyed")}setupNetworkHandlers(){this.network.onConnect(t=>{this.log(`Client connected: ${t}`)}),this.network.onDisconnect((t,e)=>{this.log(`Client disconnected: ${t} (${e})`);let i=this.core.getUser(t);i&&(this.options.application.destroyUser&&this.options.application.destroyUser(this.core,i,e),this.core.removeUser(t))}),this.network.on("join",(t,e)=>{this.log(`Join request from ${t}: ${e.username}`);try{let i=e.username||`Player${t.substring(0,4)}`,s=this.core.createUser(t,i);this.options.application.initUser(this.core,s,{username:i,token:e.token}),this.network.sendToClient(t,"join_response",{success:!0,userId:t,roomId:"main"});let o=this.core.generateAllLoadPackets();this.log(`Sending ${o.length} load packets to ${t}...`),o.forEach(m=>{this.network.sendToClient(t,"load",m)});let a=s.getInputBindingsLoadPacket();a&&(this.network.sendToClient(t,"input-bindings",a),this.log(`Sent input bindings to ${t}`)),this.log(`User ${i} (${t}) joined`)}catch(i){this.log(`Failed to join: ${i}`),this.network.sendToClient(t,"join_response",{success:!1,error:"Failed to join game"})}}),this.network.on("leave",t=>{this.log(`Leave request from ${t}`),this.network.disconnectClient(t,"User left")}),this.network.on("input",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Input from unknown user: ${t}`);return}if(e.axes&&Object.entries(e.axes).forEach(([s,o])=>{i.setAxis(s,o)}),e.buttons&&Object.entries(e.buttons).forEach(([s,o])=>{i.setButton(s,o)}),e.mousePosition){let{x:s,y:o}=e.mousePosition;i.setMousePosition(s,o,!0)}e.textInputs&&Array.isArray(e.textInputs)&&i.setTextInputs(e.textInputs)})}tick(){if(!this.running)return;let t=Date.now(),e=(t-this.lastTimestamp)/1e3;this.lastTimestamp=t,this.tickCount++,t-this.tpsUpdateTime>=1e3&&(this.tps=Math.round(this.tickCount*1e3/(t-this.tpsUpdateTime)),this.tickCount=0,this.tpsUpdateTime=t),this.options.application.update&&this.options.application.update(this.core,e),this.core.getUsers().forEach(s=>{this.options.application.updateUser(this.core,s,e),s.clearTextInputs()}),this.core.endTick(),this.broadcastUpdates()}broadcastUpdates(){let t=this.core.endTickSplit(),e=this.core.getCurrentTick();t.forEach(({static:i,dynamic:s},o)=>{if(i&&(this.network.sendToClient(o,"update-static",i),console.warn(`[SERVER] update-static: ${i.length}B (tick ${e})`)),s){let a=this.network;a.sendToClientVolatile?a.sendToClientVolatile(o,"update-dynamic",s):this.network.sendToClient(o,"update-dynamic",s),console.warn(`[SERVER] update-dynamic: ${s.length}B (tick ${e})`)}})}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}};l(c,"ServerRuntime");var p=c;0&&(module.exports={ServerRuntime});
@@ -0,0 +1,143 @@
1
+ export { IApplication } from '@utsp/types';
2
+
3
+ /**
4
+ * Server Runtime Options
5
+ */
6
+ interface ServerRuntimeOptions {
7
+ /**
8
+ * The application implementation
9
+ */
10
+ application: any;
11
+ /**
12
+ * Server port
13
+ */
14
+ port: number;
15
+ /**
16
+ * Server host
17
+ * @default '0.0.0.0'
18
+ */
19
+ host?: string;
20
+ /**
21
+ * Server tick rate (updates per second)
22
+ * @default 60
23
+ */
24
+ tickRate?: number;
25
+ /**
26
+ * Maximum concurrent connections
27
+ * @default 100
28
+ */
29
+ maxConnections?: number;
30
+ /**
31
+ * Terminal width (columns)
32
+ * @default 80
33
+ */
34
+ width?: number;
35
+ /**
36
+ * Terminal height (rows)
37
+ * @default 25
38
+ */
39
+ height?: number;
40
+ /**
41
+ * CORS configuration
42
+ * @default { origin: '*' }
43
+ */
44
+ cors?: {
45
+ origin: string | string[];
46
+ credentials?: boolean;
47
+ };
48
+ /**
49
+ * Enable debug logging
50
+ * @default false
51
+ */
52
+ debug?: boolean;
53
+ }
54
+ /**
55
+ * Server Runtime Statistics
56
+ */
57
+ interface ServerRuntimeStats {
58
+ /** Runtime mode */
59
+ mode: 'server';
60
+ /** Whether the runtime is running */
61
+ running: boolean;
62
+ /** Number of connected users */
63
+ userCount: number;
64
+ /** Current tick rate (TPS) */
65
+ tps: number;
66
+ /** Uptime in milliseconds */
67
+ uptime: number;
68
+ /** Total ticks processed */
69
+ totalTicks: number;
70
+ }
71
+
72
+ /**
73
+ * Server Runtime
74
+ *
75
+ * Headless mode - runs server without rendering.
76
+ * Uses: Core + Network (SocketIOServer)
77
+ * No Render, no Input (receives input from clients)
78
+ *
79
+ * Perfect for:
80
+ * - Multiplayer game servers
81
+ * - Authoritative game logic
82
+ * - Scalable backend
83
+ */
84
+
85
+ declare class ServerRuntime {
86
+ private core;
87
+ private network;
88
+ private options;
89
+ private running;
90
+ private startTime;
91
+ private lastTimestamp;
92
+ private tickCount;
93
+ private tps;
94
+ private tpsUpdateTime;
95
+ private tickInterval;
96
+ constructor(options: ServerRuntimeOptions);
97
+ getMode(): 'server';
98
+ isRunning(): boolean;
99
+ /**
100
+ * Change le tick rate du serveur (peut être appelé à tout moment)
101
+ * Redémarre l'interval avec la nouvelle fréquence
102
+ *
103
+ * @param tickRate - Nouveau tick rate en TPS (ticks per second)
104
+ * @example
105
+ * ```typescript
106
+ * // Dans init() de votre application :
107
+ * init(core: UTSPCore, runtime: ServerRuntime): void {
108
+ * runtime.setTickRate(60); // Passer à 60 TPS
109
+ * }
110
+ * ```
111
+ */
112
+ setTickRate(tickRate: number): void;
113
+ /**
114
+ * Récupère le tick rate actuel
115
+ */
116
+ getTickRate(): number;
117
+ start(): Promise<void>;
118
+ stop(): Promise<void>;
119
+ getStats(): ServerRuntimeStats;
120
+ destroy(): Promise<void>;
121
+ /**
122
+ * Setup network event handlers
123
+ */
124
+ private setupNetworkHandlers;
125
+ /**
126
+ * Main tick loop
127
+ */
128
+ private tick;
129
+ /**
130
+ * Broadcast updates to all connected clients
131
+ * Uses split packets:
132
+ * - Static layers → Reliable channel (guaranteed delivery)
133
+ * - Dynamic layers → Volatile channel (can drop packets for performance)
134
+ */
135
+ private broadcastUpdates;
136
+ /**
137
+ * Debug logging
138
+ */
139
+ private log;
140
+ }
141
+
142
+ export { ServerRuntime };
143
+ export type { ServerRuntimeOptions, ServerRuntimeStats };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var c=Object.defineProperty;var g=(r,t,e)=>t in r?c(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e;var l=(r,t)=>c(r,"name",{value:t,configurable:!0});var o=(r,t,e)=>(g(r,typeof t!="symbol"?t+"":t,e),e);import{Core as m}from"@utsp/core";import{SocketIOServer as v}from"@utsp/network-server";var p=class p{constructor(t){o(this,"core");o(this,"network");o(this,"options");o(this,"running",!1);o(this,"startTime",0);o(this,"lastTimestamp",0);o(this,"tickCount",0);o(this,"tps",0);o(this,"tpsUpdateTime",0);o(this,"tickInterval",null);this.options={application:t.application,port:t.port,host:t.host??"0.0.0.0",tickRate:t.tickRate??20,maxConnections:t.maxConnections??100,debug:t.debug??!1,width:t.width??80,height:t.height??25,cors:t.cors??{origin:"*"}},this.log("Initializing ServerRuntime..."),this.core=new m({mode:"server",maxUsers:this.options.maxConnections}),this.network=new v({port:this.options.port,host:this.options.host,cors:this.options.cors,maxConnections:this.options.maxConnections,debug:this.options.debug}),this.log("ServerRuntime initialized")}getMode(){return"server"}isRunning(){return this.running}setTickRate(t){if(t<=0||t>1e3)throw new Error(`Invalid tick rate: ${t}. Must be between 1 and 1000.`);if(this.options.tickRate=t,this.log(`Tick rate changed to ${t} TPS`),this.running&&this.tickInterval){clearInterval(this.tickInterval);let e=1e3/t;this.log(`Restarting tick loop at ${t} TPS (${e}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},e)}}getTickRate(){return this.options.tickRate}async start(){if(this.running){this.log("Already running");return}this.log("Starting ServerRuntime..."),await this.network.start(),this.log(`Server listening on ${this.options.host}:${this.options.port}`),this.setupNetworkHandlers(),this.log("Calling application.init()..."),this.options.application.init(this.core,this),this.running=!0,this.startTime=Date.now(),this.lastTimestamp=this.startTime,this.tpsUpdateTime=this.startTime;let t=1e3/this.options.tickRate;this.log(`Starting tick loop at ${this.options.tickRate} TPS (${t}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},t)}async stop(){if(!this.running)return;this.log("Stopping ServerRuntime..."),this.running=!1,this.tickInterval&&(clearInterval(this.tickInterval),this.tickInterval=null),this.network.getClients().forEach(e=>{this.network.disconnectClient(e,"Server stopped")}),await this.network.stop(),this.log("ServerRuntime stopped")}getStats(){return{mode:"server",running:this.running,userCount:this.core.getUsers().length,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0,totalTicks:this.tickCount}}async destroy(){await this.stop(),this.log("Destroying ServerRuntime..."),this.options.application.destroy&&this.options.application.destroy(),await this.network.destroy(),this.log("ServerRuntime destroyed")}setupNetworkHandlers(){this.network.onConnect(t=>{this.log(`Client connected: ${t}`)}),this.network.onDisconnect((t,e)=>{this.log(`Client disconnected: ${t} (${e})`);let i=this.core.getUser(t);i&&(this.options.application.destroyUser&&this.options.application.destroyUser(this.core,i,e),this.core.removeUser(t))}),this.network.on("join",(t,e)=>{this.log(`Join request from ${t}: ${e.username}`);try{let i=e.username||`Player${t.substring(0,4)}`,s=this.core.createUser(t,i);this.options.application.initUser(this.core,s,{username:i,token:e.token}),this.network.sendToClient(t,"join_response",{success:!0,userId:t,roomId:"main"});let n=this.core.generateAllLoadPackets();this.log(`Sending ${n.length} load packets to ${t}...`),n.forEach(u=>{this.network.sendToClient(t,"load",u)});let a=s.getInputBindingsLoadPacket();a&&(this.network.sendToClient(t,"input-bindings",a),this.log(`Sent input bindings to ${t}`)),this.log(`User ${i} (${t}) joined`)}catch(i){this.log(`Failed to join: ${i}`),this.network.sendToClient(t,"join_response",{success:!1,error:"Failed to join game"})}}),this.network.on("leave",t=>{this.log(`Leave request from ${t}`),this.network.disconnectClient(t,"User left")}),this.network.on("input",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Input from unknown user: ${t}`);return}if(e.axes&&Object.entries(e.axes).forEach(([s,n])=>{i.setAxis(s,n)}),e.buttons&&Object.entries(e.buttons).forEach(([s,n])=>{i.setButton(s,n)}),e.mousePosition){let{x:s,y:n}=e.mousePosition;i.setMousePosition(s,n,!0)}e.textInputs&&Array.isArray(e.textInputs)&&i.setTextInputs(e.textInputs)})}tick(){if(!this.running)return;let t=Date.now(),e=(t-this.lastTimestamp)/1e3;this.lastTimestamp=t,this.tickCount++,t-this.tpsUpdateTime>=1e3&&(this.tps=Math.round(this.tickCount*1e3/(t-this.tpsUpdateTime)),this.tickCount=0,this.tpsUpdateTime=t),this.options.application.update&&this.options.application.update(this.core,e),this.core.getUsers().forEach(s=>{this.options.application.updateUser(this.core,s,e),s.clearTextInputs()}),this.core.endTick(),this.broadcastUpdates()}broadcastUpdates(){let t=this.core.endTickSplit(),e=this.core.getCurrentTick();t.forEach(({static:i,dynamic:s},n)=>{if(i&&(this.network.sendToClient(n,"update-static",i),console.warn(`[SERVER] update-static: ${i.length}B (tick ${e})`)),s){let a=this.network;a.sendToClientVolatile?a.sendToClientVolatile(n,"update-dynamic",s):this.network.sendToClient(n,"update-dynamic",s),console.warn(`[SERVER] update-dynamic: ${s.length}B (tick ${e})`)}})}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}};l(p,"ServerRuntime");var h=p;export{h as ServerRuntime};
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@utsp/runtime-server",
3
+ "version": "0.1.1",
4
+ "description": "Server-side runtime for UTSP applications (Node.js, headless)",
5
+ "author": "Thomas Piquet",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/thp-software/utsp.git",
21
+ "directory": "packages/runtime-server"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/thp-software/utsp/issues"
25
+ },
26
+ "homepage": "https://github.com/thp-software/utsp/tree/master/packages/runtime-server#readme",
27
+ "keywords": [
28
+ "utsp",
29
+ "runtime",
30
+ "server",
31
+ "multi-user",
32
+ "terminal",
33
+ "nodejs",
34
+ "headless"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "sideEffects": false,
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@utsp/core": "0.1.1",
50
+ "@utsp/network-server": "0.1.1",
51
+ "@utsp/types": "0.1.1"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.0.0",
55
+ "typescript": "^5.3.3"
56
+ },
57
+ "scripts": {
58
+ "build": "node ../../scripts/build-package.mjs packages/runtime-server",
59
+ "dev": "tsc --watch",
60
+ "clean": "rimraf dist",
61
+ "lint": "eslint \"src/**/*.ts\" --max-warnings 0",
62
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
63
+ "typecheck": "tsc --noEmit"
64
+ }
65
+ }