{"id":25477324,"url":"https://github.com/r-unic/tether","last_synced_at":"2025-07-20T06:33:14.501Z","repository":{"id":277266330,"uuid":"931897969","full_name":"R-unic/tether","owner":"R-unic","description":"A message-based networking solution for Roblox with automatic binary serialization and type validation","archived":false,"fork":false,"pushed_at":"2025-07-15T19:18:57.000Z","size":183,"stargazers_count":2,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-19T05:34:29.322Z","etag":null,"topics":["middleware","networking","rbxts","roblox","serialization","validation"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@rbxts/tether","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/R-unic.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-02-13T03:11:15.000Z","updated_at":"2025-07-15T19:19:01.000Z","dependencies_parsed_at":"2025-02-13T03:19:53.452Z","dependency_job_id":"be11a4b9-3497-495f-8b7b-4e084aa77a71","html_url":"https://github.com/R-unic/tether","commit_stats":null,"previous_names":["r-unic/tether"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/R-unic/tether","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/R-unic%2Ftether","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/R-unic%2Ftether/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/R-unic%2Ftether/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/R-unic%2Ftether/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/R-unic","download_url":"https://codeload.github.com/R-unic/tether/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/R-unic%2Ftether/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265909593,"owners_count":23847489,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["middleware","networking","rbxts","roblox","serialization","validation"],"created_at":"2025-02-18T13:39:39.918Z","updated_at":"2025-07-20T06:33:14.474Z","avatar_url":"https://github.com/R-unic.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Tether\n\nA message-based networking solution for Roblox with automatic binary serialization and type validation.\n\nThis package uses [Serio](https://www.npmjs.com/package/@rbxts/serio) for binary serialization, so to find more info on schemas, check it out!\n\n\u003e [!CAUTION]\n\u003e Depends on `rbxts-transformer-flamework`!\n\n## Usage\n\n### In `shared/messaging.ts`\n\n```ts\nimport { MessageEmitter } from \"@rbxts/tether\";\nimport type { Packed, u8 } from \"@rbxts/serio\";\n\nexport const messaging = MessageEmitter.create\u003cMessageData\u003e();\n\nexport const enum Message {\n  Test,\n  Packed,\n}\n\nexport interface MessageData {\n  [Message.Test]: {\n    readonly foo: string;\n    readonly n: u8;\n  };\n  [Message.Packed]: Packed\u003c{\n    readonly boolean1: boolean;\n    readonly boolean2: boolean;\n    readonly boolean3: boolean;\n    readonly boolean4: boolean;\n    readonly boolean5: boolean;\n    readonly boolean6: boolean;\n    readonly boolean7: boolean;\n    readonly boolean8: boolean;\n  }\u003e;\n}\n```\n\n\u003e [!CAUTION]\n\u003e Every single message kind must implement an interface for it's data (in the example that would be the object types in `MessageData`). Message serialization (as well as your message itself) will not work if you don't do this.\n\n### Server\n\n```ts\nimport { Message, messaging } from \"shared/messaging\";\n\nmessaging.server.on(Message.Test, (player, data) =\u003e\n  print(player, \"sent data:\", data)\n);\n```\n\n### Client\n\n```ts\nimport { Message, messaging } from \"shared/messaging\";\n\nmessaging.server.emit(Message.Test, {\n  foo: \"bar\",\n  n: 69,\n});\n```\n\n## Simulated Remote Functions\n\nTether does not directly use RemoteFunctions since it's based on the MessageEmitter structure. However I have created a small framework to simulate remote functions, as shown below.\n\nFor each function you will need two messages. One to invoke the function, and one to send the return value back (which is done automatically).\n\n### In `shared/messaging.ts`\n\n```ts\nimport { MessageEmitter } from \"@rbxts/tether\";\nimport type { u8 } from \"@rbxts/serio\";\n\nexport const messaging = MessageEmitter.create\u003cMessageData\u003e();\n\nexport const enum Message {\n  Increment,\n  IncrementReturn,\n}\n\nexport interface MessageData {\n  [Message.Increment]: u8;\n  [Message.IncrementReturn]: u8;\n}\n```\n\n### Server\n\n```ts\nimport { Message, messaging } from \"shared/messaging\";\n\nmessaging.server.setCallback(\n  Message.Increment,\n  Message.IncrementReturn,\n  (_, n) =\u003e n + 1\n);\n```\n\n### Client\n\n```ts\nimport { Message, messaging } from \"shared/messaging\";\n\nmessaging.server\n  .invoke(Message.Increment, Message.IncrementReturn, 69)\n  .then(print); // 70 - incremented by the server\n\n// or use await style\nasync function main(): Promise\u003cvoid\u003e {\n  const value = await messaging.server.invoke(\n    Message.Increment,\n    Message.IncrementReturn,\n    69\n  );\n  print(value); // 70\n}\n\nmain();\n```\n\n## Middleware\n\nDrop, delay, or modify requests\n\n### Creating middleware\n\n**Note:** These client/server middlewares can be implemented as shared middlewares. This is strictly an example.\n\n#### Client\n\n```ts\nimport type { ClientMiddleware } from \"@rbxts/tether\";\n\nexport function logClient(): ClientMiddleware {\n  return (player, ctx) =\u003e\n    print(\n      `[LOG]: Sent message '${ctx.message}' to player ${player} with data:`,\n      ctx.data\n    );\n}\n```\n\n#### Server\n\n```ts\nimport type { ServerMiddleware } from \"@rbxts/tether\";\n\nexport function logServer(): ServerMiddleware {\n  return (ctx) =\u003e\n    print(\n      `[LOG]: Sent message '${ctx.message}' to server with data:`,\n      ctx.data\n    );\n}\n```\n\n#### Shared\n\n```ts\nimport { type SharedMiddleware, DropRequest } from \"@rbxts/tether\";\n\nexport function rateLimit(interval: number): SharedMiddleware {\n  let lastRequest = 0;\n\n  return () =\u003e {\n    if (os.clock() - lastRequest \u003c interval) return DropRequest;\n\n    lastRequest = os.clock();\n  };\n}\n```\n\n#### Transforming data\n\n```ts\nimport type { ServerMiddleware } from \"@rbxts/tether\";\n\nexport function incrementNumberData(): ServerMiddleware\u003cnumber\u003e {\n  // sets the data to be used by the any subsequent middlewares as well as sent through the remote\n  return (ctx) =\u003e ctx.data++;\n}\n```\n\n### Using middleware\n\n```ts\nimport { MessageEmitter, BuiltinMiddlewares } from \"@rbxts/tether\";\nimport type { Packed, u8 } from \"@rbxts/serio\";\n\nexport const messaging = MessageEmitter.create\u003cMessageData\u003e();\nmessaging.middleware\n  // only allows requests to the server every 5 seconds,\n  // drops any requests that occur within 5 seconds of each other\n  .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))\n  // will be just one byte!\n  .useShared(Message.Packed, ctx =\u003e print(\"Packed object size:\", buffer.len(ctx.getRawData().buffer)));\n  // logs every message fired\n  .useServerGlobal(logServer())\n  .useClientGlobal(logClient())\n  .useSharedGlobal(BuiltinMiddlewares.debug()); // verbosely logs every packet sent\n  .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number\n  .useServerGlobal(incrementNumberData()); // error! - global data type is always 'unknown', we cannot guarantee a number\n\nexport const enum Message {\n  Test,\n  Packed\n}\n\nexport interface MessageData {\n  [Message.Test]: {\n    readonly foo: string;\n    readonly n: u8;\n  };\n  [Message.Packed]: Packed\u003c{\n    readonly boolean1: boolean;\n    readonly boolean2: boolean;\n    readonly boolean3: boolean;\n    readonly boolean4: boolean;\n    readonly boolean5: boolean;\n    readonly boolean6: boolean;\n    readonly boolean7: boolean;\n    readonly boolean8: boolean;\n  }\u003e;\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fr-unic%2Ftether","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fr-unic%2Ftether","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fr-unic%2Ftether/lists"}