├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example ├── bin │ └── launch.js ├── chatroom │ ├── README.md │ ├── client.ts │ ├── schema.ts │ └── server.ts ├── ctf │ ├── client.ts │ ├── game.ts │ ├── schema.ts │ └── server.ts ├── package.json ├── puzzle │ ├── client.ts │ ├── schema.ts │ ├── server.ts │ └── ui │ │ └── index.tsx ├── snake │ ├── client.ts │ ├── schema.ts │ ├── server.ts │ └── snakeAPI.ts ├── todo │ ├── client.ts │ ├── schema.ts │ └── server.ts ├── trackpad │ ├── client.ts │ ├── schema.ts │ └── server.ts ├── tsconfig.json └── tslint.json ├── package.json ├── src ├── README.md ├── client.ts ├── index.ts ├── logger.ts ├── protocol.ts ├── rda │ ├── README.md │ ├── _id.ts │ ├── constant.ts │ ├── index.ts │ ├── list.ts │ ├── map.ts │ ├── rda.ts │ ├── register.ts │ ├── struct.ts │ └── test │ │ ├── apply.ts │ │ ├── constrain.ts │ │ ├── ids.ts │ │ ├── inverse.ts │ │ └── store.ts ├── replica │ ├── client.ts │ ├── index.ts │ ├── schema.ts │ └── server.ts ├── rpc │ ├── README.md │ ├── client.ts │ ├── http │ │ ├── client-browser.ts │ │ ├── client-fetch.ts │ │ ├── client.ts │ │ └── server.ts │ ├── intercept.ts │ ├── local │ │ └── index.ts │ ├── protocol.ts │ ├── server.ts │ ├── service-worker │ │ └── index.ts │ └── test │ │ ├── basic.ts │ │ └── http.ts ├── scheduler │ ├── README.md │ ├── index.ts │ ├── mock.ts │ ├── perf-now.ts │ ├── pq.ts │ ├── scheduler.ts │ ├── system.ts │ └── test │ │ ├── mock.ts │ │ └── system.ts ├── schema │ ├── README.md │ ├── _number.ts │ ├── _string.ts │ ├── array.ts │ ├── ascii.ts │ ├── bench │ │ ├── _do.ts │ │ ├── array.ts │ │ ├── date.ts │ │ ├── dictionary.ts │ │ ├── sorted-array.ts │ │ ├── struct.ts │ │ └── vector.ts │ ├── boolean.ts │ ├── bytes.ts │ ├── date.ts │ ├── dictionary.ts │ ├── fixed-ascii.ts │ ├── float32.ts │ ├── float64.ts │ ├── index.ts │ ├── int16.ts │ ├── int32.ts │ ├── int8.ts │ ├── is-primitive.ts │ ├── json.ts │ ├── nullable.ts │ ├── option.ts │ ├── quantized-float.ts │ ├── rvarint.ts │ ├── schema.ts │ ├── sorted-array.ts │ ├── struct.ts │ ├── test │ │ ├── alloc.ts │ │ ├── assign.ts │ │ ├── capacity.ts │ │ ├── clone.ts │ │ ├── equal.ts │ │ ├── free.ts │ │ ├── identity.ts │ │ ├── json.ts │ │ ├── memory.ts │ │ ├── muType.ts │ │ ├── serialization.ts │ │ └── stats.ts │ ├── trace.ts │ ├── uint16.ts │ ├── uint32.ts │ ├── uint8.ts │ ├── union.ts │ ├── utf8.ts │ ├── varint.ts │ ├── vector.ts │ └── void.ts ├── server.ts ├── socket │ ├── README.md │ ├── debug │ │ ├── README.md │ │ ├── index.ts │ │ └── test │ │ │ ├── _ │ │ │ └── schema.ts │ │ │ ├── local.ts │ │ │ └── web.ts │ ├── local │ │ ├── README.md │ │ ├── index.ts │ │ └── test │ │ │ ├── client.ts │ │ │ ├── connection.ts │ │ │ └── server.ts │ ├── multiplex.ts │ ├── net │ │ ├── README.md │ │ ├── client.ts │ │ ├── server.ts │ │ ├── test │ │ │ ├── client.ts │ │ │ └── server.ts │ │ └── util.ts │ ├── socket.ts │ ├── web │ │ ├── README.md │ │ ├── client.ts │ │ ├── server.ts │ │ └── test │ │ │ ├── client.ts │ │ │ └── server.ts │ ├── webrtc │ │ ├── README.md │ │ ├── client.ts │ │ ├── rtc.ts │ │ └── server.ts │ └── worker │ │ ├── README.md │ │ ├── client.ts │ │ ├── server.ts │ │ └── test │ │ ├── _ │ │ ├── client.ts │ │ └── worker.ts │ │ ├── client.ts │ │ └── server.ts ├── stream │ ├── README.md │ ├── codec.ts │ ├── index.ts │ └── test │ │ └── write_read.ts └── util │ ├── error.ts │ ├── parse-body.ts │ ├── port.ts │ ├── random.ts │ ├── stringify.ts │ └── test │ ├── error.ts │ └── stringify.ts ├── tool └── mudo │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── bin │ └── mudo.js │ ├── package.json │ ├── src │ ├── bin │ │ └── mudo.ts │ └── mudo.ts │ ├── tsconfig.json │ └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.json] 14 | indent_size = 2 15 | 16 | [*.md] 17 | max_line_length = 0 18 | trim_trailing_whitespace = false 19 | 20 | [COMMIT_EDITMSG] 21 | max_line_length = 0 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | Test: 9 | name: ${{ matrix.os }} Node ${{ matrix.node }} 10 | runs-on: ${{ matrix.os }} 11 | timeout-minutes: 120 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-18.04] 16 | node: [8, 10, 12, 13, 14] 17 | include: 18 | - os: macos-latest 19 | node: 14 20 | steps: 21 | - name: Checkout HEAD 22 | uses: actions/checkout@v2 23 | - name: Install dependencies 24 | run: npm i 25 | - name: Compile 26 | run: npm run build 27 | - name: Test 28 | run: npm run test-all 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependency 2 | node_modules 3 | 4 | # log 5 | logs 6 | *.log 7 | *npm-debug.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # view config 13 | .DS_Store 14 | Desktop.ini 15 | 16 | # misc 17 | .jsbeautifyrc 18 | .vscode 19 | *package-lock.json 20 | *yarn.lock 21 | coverage 22 | 23 | # build 24 | build 25 | *.d.ts 26 | *.js 27 | *.map 28 | 29 | # unignore 30 | !src/*.js 31 | !bin/*.js 32 | !**/bin/*.js 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # dependency 2 | node_modules 3 | 4 | # log 5 | logs 6 | *.log 7 | *npm-debug.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # view config 13 | .DS_Store 14 | Desktop.ini 15 | 16 | # misc 17 | .editorconfig 18 | .github 19 | .jsbeautifyrc 20 | .vscode 21 | *package-lock.json 22 | *yarn.lock 23 | coverage 24 | CODE_OF_CONDUCT.md 25 | CONTRIBUTING.md 26 | 27 | # dev 28 | bin 29 | build 30 | example 31 | tool 32 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | * Other unethical or unprofessional conduct 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.2.0, available at https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | By participating, you are expected to uphold [Code of Conduct](CODE_OF_CONDUCT.md). 3 | 4 | * [setup](#setup) 5 | * [compile](#compile) 6 | * [test](#test) 7 | * [coverage](#coverage) 8 | * [release](#release) 9 | * [coding style](#coding-style) 10 | 11 | ## setup 12 | ``` 13 | git clone git@github.com:mikolalysenko/mudb.git && cd mudb 14 | npm i 15 | npm run watch 16 | ``` 17 | 18 | ## compile 19 | ``` 20 | npm run build 21 | ``` 22 | 23 | ## test 24 | ``` 25 | # run all tests 26 | npm run test-all 27 | 28 | # run tests in a module 29 | npm run test [module] 30 | # run tests in rda 31 | npm run test rda 32 | # run tests in socket/web 33 | npm run test socket/web 34 | 35 | # run tests in a file 36 | npm run test [module test_file] 37 | # run tests in rda/test/inverse.ts 38 | npm run test rda inverse 39 | # run tests in socket/web/test/server.ts 40 | npm run test socket/web server 41 | ``` 42 | 43 | ## coverage 44 | ``` 45 | npm run coverage [module] 46 | open coverage/index.html 47 | ``` 48 | 49 | ## release 50 | ``` 51 | npm run release 52 | ``` 53 | 54 | ## coding style 55 | 56 | ### naming 57 | * Name types and enums in PascalCase. 58 | * Name variables, properties, and functions in camelCase. 59 | * Name constant in SCREAMING_SNAKE_CASE. 60 | * Prefix private properties with `_`. 61 | * Prefer whole words in names. 62 | 63 | ### whitespace 64 | * Indent with 4 spaces. 65 | * **No spaces around** the colon between name and type. 66 | ```ts 67 | class DBEvent { 68 | type:string; 69 | payload:any; 70 | timestamp:number; 71 | constructor(type:string, payload:any) { } 72 | } 73 | ``` 74 | * But do put a space **after** the function name and spaces **around** the colon for function return types. 75 | ```ts 76 | function noop () : void { } 77 | ``` 78 | 79 | ### misc 80 | * Use `const` by default. 81 | * Use single quotes for strings. Use template literals instead of concatenations. 82 | * Always surround loop and conditional bodies with curly braces. 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Mikola Lysenko, Shenzhen DianMao Digital Technology Co., Ltd. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mudb 2 | a collection of modules for building realtime client-server applications consisting of multiple protocols 3 | 4 | ## concepts 5 | 6 | ### protocol 7 | A *protocol* in the `mudb` sense, is a collection of related messages and handlers which are grouped by functionalities or whatever. 8 | 9 | ### message 10 | [Message passing](https://en.wikipedia.org/wiki/Message_passing) is the basic building block for communication in a distributed system. In `mudb`, *messages* are strongly typed using user-defined *schemas*. `mudb` provides a [reliable](https://en.wikipedia.org/wiki/Reliability_(computer_networking)), ordered message delivery and a faster but unreliable method for sending messages immediately. 11 | 12 | ### schema 13 | A *schema* is a type declaration for the interface between client and server. 14 | 15 | ### socket 16 | `mudb` communicates over the generic socket abstractions. *Sockets* support both reliable and unreliable delivery. Reliable delivery is used for messages, while unreliable delivery is used for state replication. Unreliable delivery is generally faster since it does not suffer from head-of-line blocking problems. 17 | 18 | ## modules 19 | 20 | * [core](src) 21 | * [rda](src/rda) 22 | * [replica](src/replica) 23 | * [rpc](src/rpc) 24 | * [scheduler](src/scheduler) 25 | * [schema](src/schema) 26 | * [socket](src/socket) 27 | * [local](src/socket/local) 28 | * [uws](src/socket/uws) 29 | * [web](src/socket/web) 30 | * [stream](src/stream) 31 | 32 | ## examples 33 | 34 | * [chatroom](example/chatroom) 35 | * [ctf](example/ctf) 36 | * [puzzle](example/puzzle) 37 | * [snake](example/snake) 38 | * [todo](example/todo) 39 | * [trackpad](example/trackpad) 40 | 41 | ## further reading 42 | Light reading: 43 | * [Protocol Buffers](https://developers.google.com/protocol-buffers) 44 | * [Quake 3 network model](http://fabiensanglard.net/quake3/network.php) 45 | * [Janus](http://equis.cs.queensu.ca/wiki/index.php/Janus) 46 | * "[The Tech of Planetary Annihilation: ChronoCam](https://blog.forrestthewoods.com/the-tech-of-planetary-annihilation-chronocam-292e3d6b169a)" 47 | * "[The Implementation of Rewind in Braid](https://www.youtube.com/watch?v=8dinUbg2h70)" 48 | * "[Relativistic Replication](https://mikolalysenko.github.io/nodeconfeu2014-slides/index.html#/)" 49 | * "[Source Multiplayer Networking](https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking)" 50 | 51 | Academic references: 52 | * C. Savery, T.C. Graham, "[Timelines: Simplifying the programming of lag compensation for the next generation of networked games](https://link.springer.com/article/10.1007/s00530-012-0271-3)" 2013 53 | * Local perception filters 54 | 55 | ## FAQ 56 | [FAQ](https://github.com/mikolalysenko/mudb/issues?q=is%3Aissue+label%3AFAQ+) 57 | 58 | ## credits 59 | Development supported by Shenzhen DianMao Digital Technology Co., Ltd. 60 | 61 | Copyright (c) 2020 Mikola Lysenko, Shenzhen DianMao Digital Technology Co., Ltd. 62 | -------------------------------------------------------------------------------- /example/bin/launch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path') 3 | const exec = require('child_process').exec 4 | 5 | const argv = process.argv.slice(2) 6 | const exp = argv[0] 7 | if (!exp) { 8 | throw new Error(`example name is missing`) 9 | } 10 | 11 | const mudoDir = path.join(path.resolve('..'), 'tool/mudo') 12 | const expDir = path.join(path.resolve('.'), exp) 13 | const sockType = argv[1] || 'web' 14 | const cmd = `node ${mudoDir}/bin/mudo.js --client ${expDir}/client.js --server ${expDir}/server.js --socket ${sockType} --open` 15 | 16 | exec(cmd, (err, stdout) => { 17 | if (err) { 18 | console.error(err) 19 | return 20 | } 21 | console.log(stdout) 22 | }) 23 | -------------------------------------------------------------------------------- /example/chatroom/README.md: -------------------------------------------------------------------------------- 1 | A minimal CLI chatroom as an example of defining protocols in `mudb` and using `mudb` net socket. 2 | 3 | ## Setup 4 | If you haven't done this, 5 | 1. `npm i -g tsc` 6 | 2. `cd mudb/example` 7 | 3. `npm i && tsc` 8 | 9 | ## Run 10 | 1. `cd chatroom` 11 | 2. `node server.js` 12 | 3. open another terminal and cd into the chatroom directory 13 | 4. `node client.js` 14 | 15 | You'll be prompted to enter your nickname. After entering the chatroom, you can also change you nickname by entering `/nick newNick`. If you feel lonely in the chatroom, repeat 3 & 4 however many times you like. 16 | -------------------------------------------------------------------------------- /example/chatroom/client.ts: -------------------------------------------------------------------------------- 1 | import readline = require('readline'); 2 | 3 | import { MuNetSocket } from 'mudb/socket/net/client'; 4 | import { MuClient } from 'mudb/client'; 5 | import { ChatSchema } from './schema'; 6 | 7 | const socket = new MuNetSocket({ 8 | sessionId: Math.random().toString(36).substring(2), 9 | // for TCP connection 10 | connectOpts: { 11 | host: '127.0.0.1', 12 | port: 9977, 13 | }, 14 | // for UDP binding 15 | bindOpts: { 16 | address: '127.0.0.1', 17 | port: 9989, 18 | }, 19 | }); 20 | const client = new MuClient(socket); 21 | 22 | let nickname:string; 23 | 24 | // protocols should be defined and configured before 25 | // client is started 26 | const protocol = client.protocol(ChatSchema); 27 | protocol.configure({ 28 | ready: () => { 29 | protocol.server.message.join(nickname); 30 | 31 | readline.createInterface({ 32 | input: process.stdin, 33 | output: process.stdout, 34 | prompt: '', 35 | }).on('line', (msg) => { 36 | process.stdout.write('\x1b[1A\x1b[K'); 37 | 38 | // change nickname 39 | if (/^\/nick /.test(msg)) { 40 | const m = msg.match(/^\/nick (.+)/); 41 | if (m) { 42 | const nick = m[1]; 43 | protocol.server.message.nick(nick); 44 | return; 45 | } 46 | } 47 | // say something 48 | protocol.server.message.say(msg); 49 | }); 50 | }, 51 | message: { 52 | chat: ({ name, msg }) => { 53 | console.log(`${name}: ${msg}`); 54 | }, 55 | notice: (n) => { 56 | console.log(n); 57 | }, 58 | }, 59 | close: () => { }, 60 | }); 61 | 62 | const prompt = readline.createInterface({ 63 | input: process.stdin, 64 | output: process.stdout, 65 | }); 66 | prompt.question('Nickname: ', (n) => { 67 | nickname = n; 68 | prompt.close(); 69 | client.start(); 70 | }); 71 | -------------------------------------------------------------------------------- /example/chatroom/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuUTF8, 3 | MuStruct, 4 | } from 'mudb/schema'; 5 | 6 | export const ChatSchema = { 7 | client: { 8 | chat: new MuStruct({ 9 | name: new MuUTF8(), 10 | msg: new MuUTF8(), 11 | }), 12 | notice: new MuUTF8(), 13 | }, 14 | server: { 15 | join: new MuUTF8(), 16 | nick: new MuUTF8(), 17 | say: new MuUTF8(), 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /example/chatroom/server.ts: -------------------------------------------------------------------------------- 1 | import tcp = require('net'); 2 | import udp = require('dgram'); 3 | 4 | import { MuNetSocketServer } from 'mudb/socket/net/server'; 5 | import { MuServer } from 'mudb/server'; 6 | import { ChatSchema } from './schema'; 7 | 8 | const tcpServer = tcp.createServer(); 9 | const udpServer = udp.createSocket({ type: 'udp4' }); 10 | const socketServer = new MuNetSocketServer({ 11 | tcpServer, 12 | udpServer, 13 | }); 14 | const server = new MuServer(socketServer); 15 | 16 | const idToNick:{ [sessionId:string]:string } = {}; 17 | 18 | const protocol = server.protocol(ChatSchema); 19 | protocol.configure({ 20 | connect: () => { }, 21 | disconnect: (client) => { 22 | const sessionId = client.sessionId; 23 | protocol.broadcast.notice(`${idToNick[sessionId]} has left`); 24 | delete idToNick[sessionId]; 25 | }, 26 | message: { 27 | join: (client, nickname) => { 28 | idToNick[client.sessionId] = nickname; 29 | protocol.broadcast.notice(`${nickname} has joined`); 30 | }, 31 | say: (client, msg) => { 32 | protocol.broadcast.chat({ 33 | name: idToNick[client.sessionId], 34 | msg, 35 | }); 36 | }, 37 | nick: (client, nickname) => { 38 | const sessionId = client.sessionId; 39 | protocol.broadcast.notice(`${idToNick[sessionId]} is now known as ${nickname}`); 40 | idToNick[sessionId] = nickname; 41 | }, 42 | }, 43 | close: () => { 44 | console.log('server closed'); 45 | }, 46 | }); 47 | 48 | tcpServer.listen(9977, '127.0.0.1', () => { 49 | console.log(`server listening on port ${tcpServer.address().port}...`); 50 | }); 51 | udpServer.bind(9988, '127.0.0.1'); 52 | server.start(); 53 | -------------------------------------------------------------------------------- /example/ctf/client.ts: -------------------------------------------------------------------------------- 1 | import { MuClient } from 'mudb/client'; 2 | import { MuClientState } from 'mudb/state/client'; 3 | import { MuRPCClient } from 'mudb/rpc/client'; 4 | 5 | import { StateSchema, MsgSchema, RpcSchema } from './schema'; 6 | import { Map, Player, Flag, Team, Direction, Config } from './game'; 7 | 8 | export = function(client:MuClient) { 9 | const canvas = document.createElement('canvas'); 10 | canvas.style.padding = '0px'; 11 | canvas.style.margin = '0px'; 12 | canvas.style.backgroundColor = 'black'; 13 | canvas.width = Config.canvas_width; 14 | canvas.height = Config.canvas_height; 15 | document.body.appendChild(canvas); 16 | 17 | const ctx = canvas.getContext('2d'); 18 | if (!ctx) { 19 | document.body.innerText = 'canvas not supported'; 20 | return; 21 | } 22 | 23 | const stateProtocol = new MuClientState({ 24 | schema: StateSchema, 25 | client, 26 | }); 27 | const msgProtocol = client.protocol(MsgSchema); 28 | const rpcProtocol = new MuRPCClient(client, RpcSchema); 29 | 30 | const map = new Map(canvas.width, canvas.height); 31 | let myPlayer; 32 | let players = {}; 33 | let raf; 34 | 35 | stateProtocol.configure({ 36 | ready: () => { 37 | console.log(client.sessionId); 38 | 39 | rpcProtocol.server.rpc.joinTeam(client.sessionId, (teamGroup) => { 40 | const {x, y} = getInitPosition(teamGroup); 41 | myPlayer = new Player(x, y, teamGroup); 42 | runGame(); 43 | 44 | document.addEventListener('keydown', function(e) { 45 | if (e.keyCode === 27) { // ESC 46 | window.cancelAnimationFrame(raf); 47 | } else { // start 48 | myPlayer.direction = e.keyCode; 49 | } 50 | }); 51 | 52 | raf = window.requestAnimationFrame(updateCanvas); 53 | }); 54 | }, 55 | }); 56 | 57 | msgProtocol.configure({ 58 | message: { 59 | score: (score) => { 60 | map.score = score; 61 | }, 62 | dead: (id) => { 63 | if (client.sessionId === id) { 64 | const {x, y} = getInitPosition(myPlayer.team); 65 | myPlayer.x = x; 66 | myPlayer.y = y; 67 | myPlayer.direction = undefined; 68 | } 69 | }, 70 | }, 71 | }); 72 | 73 | rpcProtocol.configure({ 74 | rpc: { 75 | joinTeam: (arg, next) => { }, 76 | }, 77 | }); 78 | 79 | client.start(); 80 | 81 | function runGame() { 82 | map.draw(ctx); 83 | 84 | // draw flags 85 | for (let i = 0; i < stateProtocol.server.state.flag.length; i++) { 86 | const {x, y, team} = stateProtocol.server.state.flag[i]; 87 | const flag = new Flag(x, y, team); 88 | flag.draw(ctx); 89 | } 90 | 91 | // draw remote players 92 | players = stateProtocol.server.state.player; 93 | const playerProps = Object.keys(players); 94 | for (let i = 0; i < playerProps.length; i++) { 95 | if (playerProps[i] !== client.sessionId) { 96 | const {x, y, team} = players[playerProps[i]]; 97 | const player = new Player(x, y, team); 98 | player.draw(ctx); 99 | } 100 | } 101 | 102 | // move local player 103 | myPlayer.move(ctx); 104 | myPlayer.draw(ctx); 105 | 106 | // update state 107 | stateProtocol.state.team = myPlayer.team; 108 | stateProtocol.state.x = myPlayer.x; 109 | stateProtocol.state.y = myPlayer.y; 110 | stateProtocol.commit(); 111 | } 112 | 113 | function updateCanvas() { 114 | if (!ctx) { return; } 115 | ctx.clearRect(0, 0, canvas.width, canvas.height); 116 | runGame(); 117 | raf = window.requestAnimationFrame(updateCanvas); 118 | } 119 | 120 | function getRandomInt(min, max) { 121 | return Math.floor(Math.random() * (max - min + 1) + min); 122 | } 123 | 124 | function getInitPosition(teamGroup) { 125 | return { 126 | x: getRandomInt(10, canvas.width - 10), 127 | y: (teamGroup === Team.top) ? 10 : canvas.height - 10, 128 | }; 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /example/ctf/game.ts: -------------------------------------------------------------------------------- 1 | export enum Direction { 2 | left = 37, 3 | up, 4 | right, 5 | down, 6 | } 7 | 8 | export enum Team { 9 | top, 10 | bottom, 11 | } 12 | 13 | export const Config = { 14 | canvas_width: 700, 15 | canvas_height: 500, 16 | player_size: 10, 17 | flag_size: 15, 18 | }; 19 | 20 | export class Player { 21 | private readonly speed = 3; 22 | private readonly r = Config.player_size; //radius of face 23 | private readonly lineWidth = this.r / 8; 24 | 25 | public x:number; 26 | public y:number; 27 | public color:string; 28 | public team:Team; 29 | public direction:number|undefined; 30 | 31 | constructor(x, y, team) { 32 | this.x = x; 33 | this.y = y; 34 | this.team = team; 35 | this.color = (team === Team.top) ? 'yellow' : '#00ffde'; 36 | } 37 | 38 | public draw(ctx) { 39 | ctx.lineWidth = this.lineWidth; 40 | ctx.strokeStyle = this.color; 41 | ctx.fillStyle = this.color; 42 | 43 | ctx.beginPath(); 44 | ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, true); // face circle 45 | ctx.moveTo(this.x + this.r * 2 / 3, this.y); 46 | ctx.arc(this.x, this.y, this.r * 2 / 3, 0, Math.PI, false); // mouth 47 | ctx.stroke(); 48 | 49 | ctx.beginPath(); 50 | ctx.arc(this.x - (this.r / 3), this.y - (this.r / 3), this.r / 5, 0, Math.PI * 2, true); // left eye 51 | ctx.arc(this.x + (this.r / 3), this.y - (this.r / 3), this.r / 5, 0, Math.PI * 2, true); // right eye 52 | ctx.fill(); 53 | } 54 | 55 | public move(ctx) { 56 | switch (this.direction) { 57 | case Direction.up: 58 | this.y -= this.speed; 59 | if (this.y < this.r) { this.y = this.r; } 60 | break; 61 | case Direction.left: 62 | this.x -= this.speed; 63 | if (this.x < this.r) { this.x = this.r; } 64 | break; 65 | case Direction.right: 66 | this.x += this.speed; 67 | if (this.x > Config.canvas_width - this.r) { this.x = Config.canvas_width - this.r; } 68 | break; 69 | case Direction.down: 70 | this.y += this.speed; 71 | if (this.y > Config.canvas_height - this.r) { this.y = Config.canvas_height - this.r; } 72 | break; 73 | default: 74 | break; 75 | } 76 | this.draw(ctx); 77 | } 78 | } 79 | 80 | export class Flag { 81 | private readonly tall = Config.flag_size; 82 | 83 | public color:string; 84 | public team:Team; 85 | public x:number; 86 | public y:number; 87 | 88 | constructor(x, y, team:Team) { 89 | this.x = x; 90 | this.y = y; 91 | this.color = (team === Team.top) ? 'yellow' : '#00ffde'; 92 | this.team = team; 93 | } 94 | 95 | public draw(ctx) { 96 | ctx.strokeStyle = this.color; 97 | ctx.fillStyle = this.color; 98 | ctx.lineWidth = 5; 99 | 100 | ctx.beginPath(); 101 | ctx.moveTo(this.x, this.y); 102 | ctx.lineTo(this.x, this.y - this.tall); 103 | ctx.stroke(); 104 | ctx.lineTo(this.x + this.tall / 2, this.y - this.tall * 2 / 3); 105 | ctx.lineTo(this.x, this.y - this.tall * 1 / 3); 106 | ctx.fill(); 107 | } 108 | } 109 | 110 | export class Map { 111 | public width:number; 112 | public height:number; 113 | public score:number[]; 114 | 115 | constructor(width, height) { 116 | this.width = width; 117 | this.height = height; 118 | this.score = [0, 0]; 119 | } 120 | 121 | public draw(ctx) { 122 | ctx.strokeStyle = 'red'; 123 | ctx.beginPath(); 124 | ctx.moveTo(0, this.height / 2); 125 | ctx.lineTo(this.width, this.height / 2); 126 | ctx.stroke(); 127 | ctx.closePath(); 128 | 129 | // show score 130 | ctx.font = '48px serif'; 131 | ctx.fillStyle = 'white'; 132 | ctx.fillText(this.score[0].toString(), Config.canvas_width - 40, 35); 133 | ctx.fillText(this.score[1].toString(), Config.canvas_width - 40, Config.canvas_height - 5); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /example/ctf/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuStruct, 3 | MuDictionary, 4 | MuUTF8, 5 | MuFloat64, 6 | MuInt8, 7 | MuArray, 8 | } from 'mudb/schema'; 9 | 10 | function tuple (...args:T) : T { 11 | return args; 12 | } 13 | 14 | export const PlayerSchema = new MuStruct({ 15 | team: new MuInt8(), 16 | x: new MuFloat64(), 17 | y: new MuFloat64(), 18 | }); 19 | 20 | export const FlagSchema = new MuStruct({ 21 | team: new MuInt8(), 22 | x: new MuFloat64(), 23 | y: new MuFloat64(), 24 | }); 25 | 26 | export const StateSchema = { 27 | client: PlayerSchema, 28 | server: new MuStruct({ 29 | player: new MuDictionary(PlayerSchema, Infinity), 30 | flag: new MuArray(FlagSchema, Infinity), 31 | }), 32 | }; 33 | 34 | export const MsgSchema = { 35 | client: { 36 | score: new MuArray(new MuInt8(), Infinity), 37 | dead: new MuUTF8(), 38 | }, 39 | server: { 40 | 41 | }, 42 | }; 43 | 44 | export const RpcSchema = { 45 | client: { 46 | joinTeam: tuple(new MuUTF8(), new MuInt8()), 47 | }, 48 | server: { 49 | joinTeam: tuple(new MuUTF8(), new MuInt8()), 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "author": "Mikola Lysenko", 4 | "scripts": { 5 | "launch": "bin/launch.js" 6 | }, 7 | "dependencies": { 8 | "@types/react": "^16.8.4", 9 | "@types/react-dom": "^16.8.2", 10 | "react": "^16.8.3", 11 | "react-dom": "^16.8.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/puzzle/client.ts: -------------------------------------------------------------------------------- 1 | import { MuClient } from 'mudb/client'; 2 | import { MuReplicaClient } from 'mudb/replica/client'; 3 | import { PuzzleList } from './schema'; 4 | import { ExampleUi, PuzzleDetail } from './ui'; 5 | 6 | export = function (client:MuClient) { 7 | const replica = new MuReplicaClient({ 8 | client, 9 | rda: PuzzleList, 10 | }); 11 | 12 | replica.configure({ 13 | ready: () => { 14 | console.log('client ready'); 15 | }, 16 | change: (state) => { 17 | ui.renderDom([...state]); 18 | }, 19 | }); 20 | 21 | const setColor = (idx:number, color:string) => { 22 | const action = replica.action().update(idx).color(color); 23 | replica.dispatch( 24 | action, 25 | true, 26 | ); 27 | }; 28 | 29 | const setRotation = (idx:number, rotation:number) => { 30 | const action = replica.action().update(idx).rotation(rotation); 31 | replica.dispatch( 32 | action, 33 | true, 34 | ); 35 | }; 36 | 37 | const setPositionX = (idx:number, x:number) => { 38 | replica.dispatch( 39 | replica.action().update(idx).position.x(x), 40 | true, 41 | ); 42 | }; 43 | 44 | const setPositionY = (idx:number, y:number) => { 45 | replica.dispatch( 46 | replica.action().update(idx).position.y(y), 47 | true, 48 | ); 49 | }; 50 | 51 | const deletePuzzle = (idx:number) => { 52 | replica.dispatch( 53 | replica.action().remove(idx), 54 | ); 55 | }; 56 | 57 | const createPuzzle = (puzzle:PuzzleDetail) => { 58 | replica.dispatch( 59 | replica.action().push([puzzle]), 60 | ); 61 | }; 62 | 63 | const ui = new ExampleUi({ 64 | puzzleList: [], 65 | setX: (idx:number, x:number) => setPositionX(idx, x), 66 | setY: (idx:number, y:number) => setPositionY(idx, y), 67 | setColor: (idx:number, color:string) => setColor(idx, color), 68 | setRotation: (idx:number, rotation:number) => setRotation(idx, rotation), 69 | undo: () => replica.undo(), 70 | redo: () => replica.redo(), 71 | deletePuzzle: (idx) => deletePuzzle(idx), 72 | createPuzzle: (p) => createPuzzle(p), 73 | }); 74 | 75 | client.start(); 76 | }; -------------------------------------------------------------------------------- /example/puzzle/schema.ts: -------------------------------------------------------------------------------- 1 | import { MuRDARegister, MuRDAMap, MuRDAStruct, MuRDAConstant, MuRDAList } from 'mudb/rda'; 2 | import { MuStruct, MuInt32, MuFloat64, MuUTF8 } from 'mudb/schema'; 3 | 4 | export const PuzzlePiece = new MuRDAStruct({ 5 | color: new MuRDARegister(new MuUTF8()), 6 | position: new MuRDAStruct({ 7 | x: new MuRDARegister(new MuFloat64(0)), 8 | y: new MuRDARegister(new MuFloat64(0)), 9 | }), 10 | rotation: new MuRDARegister(new MuFloat64(0)), 11 | }); 12 | 13 | export const PuzzleList = new MuRDAList(PuzzlePiece); 14 | 15 | // export const Puzzle = new MuRDAMap(new MuUTF8(), PuzzlePiece); 16 | -------------------------------------------------------------------------------- /example/puzzle/server.ts: -------------------------------------------------------------------------------- 1 | import { MuServer } from 'mudb/server'; 2 | import { MuReplicaServer } from 'mudb/replica/server'; 3 | import { PuzzleList } from './schema'; 4 | 5 | export = function (server:MuServer) { 6 | const replica = new MuReplicaServer({ 7 | server: server, 8 | rda: PuzzleList, 9 | initialState:[ 10 | { 11 | color: 'red', 12 | position: { x: 0, y: 0 }, 13 | rotation: 0, 14 | }, 15 | { 16 | color: 'green', 17 | position: { x: 100, y: 0 }, 18 | rotation: 0, 19 | }, 20 | { 21 | color: 'blue', 22 | position: { x: 0, y: 100 }, 23 | rotation: 0, 24 | }, 25 | { 26 | color: 'yellow', 27 | position: { x: 100, y: 100 }, 28 | rotation: 0, 29 | }, 30 | ], 31 | }); 32 | replica.configure({ 33 | connect:(sessionId) => { 34 | console.log(sessionId, ' connected'); 35 | }, 36 | }); 37 | 38 | server.start(); 39 | }; 40 | -------------------------------------------------------------------------------- /example/snake/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuUTF8, 3 | MuStruct, 4 | MuFloat32, 5 | MuUnion, 6 | MuInt8, 7 | MuArray, 8 | } from 'mudb/schema'; 9 | 10 | const MuFloatOrString = new MuUnion({ 11 | float: new MuFloat32(), 12 | string: new MuUTF8(), 13 | }); 14 | 15 | export const PointSchema = new MuStruct({ 16 | x: new MuInt8(), 17 | y: new MuInt8(), 18 | }); 19 | 20 | export const SnakeSchema = new MuStruct({ 21 | id: new MuUTF8(), 22 | body: new MuArray(PointSchema, Infinity), 23 | color: new MuStruct({ 24 | head: new MuUTF8(), 25 | body: new MuUTF8(), 26 | }), 27 | }); 28 | 29 | export const GameSchema = { 30 | client: { // server to client 31 | updateFood: new MuArray(PointSchema, Infinity), 32 | updateSnakes: new MuArray(SnakeSchema, Infinity), 33 | playerDead: new MuUTF8(), 34 | newPlayer: new MuUTF8(), 35 | }, 36 | server: { // client to server 37 | redirect: new MuInt8(), 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /example/snake/server.ts: -------------------------------------------------------------------------------- 1 | import { GameSchema } from './schema'; 2 | import { MuServer } from 'mudb/server'; 3 | import { Snake, PointInterface, Food } from './snakeAPI'; 4 | 5 | export = function (server:MuServer) { 6 | const protocol = server.protocol(GameSchema); 7 | 8 | let snakes:{id:string, body:PointInterface[], color:{head:string, body:string}}[] = []; //FIXME: id can delete 9 | const allFood:PointInterface[] = []; 10 | const snakeObjs:{[id:string]:Snake} = {}; 11 | const timeInterval:number = 100; 12 | const foodNum = 3; 13 | 14 | protocol.configure({ 15 | ready: () => { 16 | for (let i = 0; i < foodNum; i++) { 17 | allFood.push(Food.new()); 18 | } 19 | 20 | setInterval(() => { 21 | snakes = []; 22 | Object.keys(snakeObjs).forEach((id) => { 23 | let snakeIsDead = false; 24 | const snake = snakeObjs[id]; 25 | snake.move( 26 | allFood, 27 | () => { protocol.broadcast.updateFood(allFood); }, 28 | () => { 29 | delete snakeObjs[id]; 30 | protocol.broadcast.playerDead(id); 31 | snakeIsDead = true; 32 | }); 33 | if (!snakeIsDead) { 34 | snakes.push(snake.toData()); 35 | } 36 | }); 37 | protocol.broadcast.updateSnakes(snakes); 38 | }, timeInterval); 39 | }, 40 | message: { // message from client 41 | redirect: (client, redirect) => { 42 | const snake = snakeObjs[client.sessionId]; 43 | if (snake) { 44 | snake.direction = redirect; 45 | } 46 | }, 47 | }, 48 | connect: (client) => { 49 | const snake = new Snake(client.sessionId, undefined, undefined, { 50 | head: getRandomColor(), 51 | body: getRandomColor(), 52 | }); 53 | snakeObjs[snake.id] = snake; 54 | snakes.push(snake.toData()); 55 | protocol.broadcast.newPlayer(client.sessionId); 56 | protocol.broadcast.updateSnakes(snakes); 57 | protocol.broadcast.updateFood(allFood); 58 | }, 59 | disconnect: (client) => { 60 | delete snakeObjs[client.sessionId]; 61 | // TODO: update snakes 62 | // protocol.broadcast.updateSnakes(snakes); 63 | }, 64 | }); 65 | 66 | function getRandomColor() : string { 67 | const letters = '0123456789ABCDEF'; 68 | let color = '#'; 69 | for (var i = 0; i < 6; i++) { 70 | color += letters[Math.floor(Math.random() * 16)]; 71 | } 72 | return color; 73 | } 74 | 75 | server.start(); 76 | }; 77 | -------------------------------------------------------------------------------- /example/todo/client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { MuClient } from 'mudb/client'; 3 | import { MuReplicaClient } from 'mudb/replica/client'; 4 | import { TodoRDA, TodoListRDA } from './schema'; 5 | import { MuUUID } from '../../rda'; 6 | 7 | enum AppState { 8 | CONNECTING, 9 | READY, 10 | CLOSED, 11 | } 12 | 13 | export = function (client:MuClient) { 14 | const model = { 15 | appState: AppState.CONNECTING, 16 | state: TodoListRDA.stateSchema.clone(TodoListRDA.stateSchema.identity), 17 | }; 18 | 19 | const replica = new MuReplicaClient({ 20 | client, 21 | rda: TodoListRDA, 22 | }); 23 | replica.configure({ 24 | ready: () => { 25 | model.appState = AppState.READY; 26 | render(); 27 | }, 28 | change: () => { 29 | model.state = replica.state(model.state); 30 | render(); 31 | }, 32 | close: () => { 33 | model.appState = AppState.CLOSED; 34 | render(); 35 | }, 36 | }); 37 | 38 | const actions = { 39 | create: (title:string) => { 40 | replica.dispatch( 41 | TodoListRDA.actions.create({ 42 | creation: new Date(), 43 | completed: false, 44 | title, 45 | })); 46 | }, 47 | setCompleted: (id:MuUUID, completed:boolean) => { 48 | replica.dispatch( 49 | TodoListRDA.actions.update( 50 | id, 51 | TodoRDA.actions.completed.set(completed))); 52 | }, 53 | rename: (id:MuUUID, title:string) => { 54 | replica.dispatch( 55 | TodoListRDA.actions.update( 56 | id, 57 | TodoRDA.actions.title.set(title))); 58 | }, 59 | destroy: (id:MuUUID) => { 60 | replica.dispatch( 61 | TodoListRDA.actions.destroy(id)); 62 | }, 63 | undo: () => { 64 | replica.dispatchUndo(); 65 | }, 66 | }; 67 | 68 | function render () { 69 | // TODO 70 | } 71 | 72 | render(); 73 | }; 74 | */ -------------------------------------------------------------------------------- /example/todo/schema.ts: -------------------------------------------------------------------------------- 1 | import { MuRDAStruct, MuRDARegister, MuRDAConstant, MuRDAList } from 'mudb/rda'; 2 | import { MuBoolean, MuUTF8, MuDate } from 'mudb/schema'; 3 | 4 | export const TodoRDA = new MuRDAStruct({ 5 | creation: new MuRDAConstant(new MuDate()), 6 | completed: new MuRDARegister(new MuBoolean(false)), 7 | title: new MuRDARegister(new MuUTF8('task')), 8 | }); 9 | 10 | export const TodoListRDA = new MuRDAList(TodoRDA); 11 | -------------------------------------------------------------------------------- /example/todo/server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { MuServer } from 'mudb/server'; 3 | import { MuReplicaServer } from 'mudb/replica/server'; 4 | import { TodoListRDA } from './schema'; 5 | 6 | export = function (server:MuServer) { 7 | const replica = new MuReplicaServer({ 8 | server, 9 | rda: TodoListRDA, 10 | }); 11 | replica.configure({ 12 | connect: (sessionId) => { 13 | console.log(`${sessionId} joined`); 14 | }, 15 | disconnect: (sessionId) => { 16 | console.log(`${sessionId} left`); 17 | }, 18 | change: (state) => { 19 | console.log(`state: ${state}`); 20 | }, 21 | }); 22 | }; 23 | */ -------------------------------------------------------------------------------- /example/trackpad/client.ts: -------------------------------------------------------------------------------- 1 | import { MuClient } from '../../client'; 2 | import { MuDeltaClient } from '../../delta/client'; 3 | import { PlayerSetSchema, ControllerSchema } from './schema'; 4 | 5 | export = function (client:MuClient) { 6 | const canvas = document.createElement('canvas'); 7 | const context = canvas.getContext('2d'); 8 | if (!context) { 9 | document.body.innerText = 'canvas not supported'; 10 | return; 11 | } 12 | 13 | canvas.style.padding = '0px'; 14 | canvas.style.margin = '0px'; 15 | canvas.width = window.innerWidth; 16 | canvas.height = window.innerHeight; 17 | document.body.appendChild(canvas); 18 | 19 | function draw (players) { 20 | if (!context) { 21 | return; 22 | } 23 | 24 | context.fillStyle = '#fff'; 25 | context.fillRect(0, 0, canvas.width, canvas.height); 26 | 27 | Object.keys(players).forEach((id) => { 28 | const { color, x, y } = players[id]; 29 | context.fillStyle = color; 30 | context.fillRect(x * canvas.width - 10, y * canvas.height - 10, 10, 10); 31 | }); 32 | } 33 | 34 | const deltaClient = new MuDeltaClient({ 35 | client: client, 36 | schema: PlayerSetSchema, 37 | }); 38 | deltaClient.configure({ 39 | change: (state) => { 40 | requestAnimationFrame(() => draw(state)); 41 | }, 42 | }); 43 | 44 | const playProtocol = client.protocol(ControllerSchema); 45 | playProtocol.configure({ 46 | ready: () => { 47 | canvas.addEventListener('mousemove', (ev) => { 48 | playProtocol.server.message.move({ 49 | x: ev.clientX / canvas.width, 50 | y: ev.clientY / canvas.height, 51 | }); 52 | }); 53 | }, 54 | message: () => { }, 55 | }); 56 | 57 | client.start(); 58 | }; 59 | -------------------------------------------------------------------------------- /example/trackpad/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuStruct, 3 | MuFloat64, 4 | MuASCII, 5 | MuDictionary, 6 | } from 'mudb/schema'; 7 | 8 | export const PlayerSchema = new MuStruct({ 9 | x: new MuFloat64(), 10 | y: new MuFloat64(), 11 | color: new MuASCII('#fff'), 12 | }); 13 | 14 | export const PlayerSetSchema = new MuDictionary(PlayerSchema, Infinity); 15 | 16 | export const ControllerSchema = { 17 | client: {}, 18 | server: { 19 | move: new MuStruct({ 20 | x: new MuFloat64(), 21 | y: new MuFloat64(), 22 | }), 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /example/trackpad/server.ts: -------------------------------------------------------------------------------- 1 | import { MuServer } from '../../server'; 2 | import { MuDeltaServer } from '../../delta/server'; 3 | import { PlayerSetSchema, ControllerSchema } from './schema'; 4 | 5 | export = (server:MuServer) => { 6 | const deltaServer = new MuDeltaServer({ 7 | server: server, 8 | schema: PlayerSetSchema, 9 | }); 10 | 11 | const players = {}; 12 | 13 | const playProtocol = server.protocol(ControllerSchema); 14 | playProtocol.configure({ 15 | connect: (client) => { 16 | players[client.sessionId] = { 17 | x: 0, 18 | y: 0, 19 | color: `#${Math.floor(Math.random() * 0xefffff + 0x100000).toString(16)}`, 20 | }; 21 | deltaServer.publish(players); 22 | }, 23 | message: { 24 | move: (client, data) => { 25 | const { x, y } = data; 26 | players[client.sessionId].x = x; 27 | players[client.sessionId].y = y; 28 | deltaServer.publish(players); 29 | }, 30 | }, 31 | disconnect: (client) => { 32 | delete players[client.sessionId]; 33 | deltaServer.publish(players); 34 | }, 35 | }); 36 | 37 | server.start(); 38 | }; 39 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["dom", "es5"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noImplicitReturns": true, 10 | "noImplicitAny": false, 11 | "outDir": ".", 12 | "plugins": [ 13 | { 14 | "name": "tslint-language-service" 15 | } 16 | ], 17 | "removeComments": true, 18 | "rootDir": ".", 19 | "sourceMap": true, 20 | "strict": true, 21 | "target": "es5", 22 | "jsx": "react" 23 | }, 24 | "include": [ 25 | "./**/*" 26 | ], 27 | "exclude": [ 28 | "build", 29 | "node_modules" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /example/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "arrow-return-shorthand": true, 5 | "array-type": [true, "array"], 6 | "await-promise": true, 7 | "curly": true, 8 | "no-arg": true, 9 | "no-conditional-assignment": true, 10 | "no-construct": true, 11 | "no-debugger": true, 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-floating-promises": true, 15 | "no-for-in-array": true, 16 | "no-misused-new": true, 17 | "no-null-keyword": false, 18 | "no-shadowed-variable": true, 19 | "no-unsafe-finally": true, 20 | "no-unused-variable": true, 21 | "no-use-before-declare": true, 22 | "indent": [true, "spaces"], 23 | "linebreak-style": [true, "LF"], 24 | "label-position": true, 25 | "no-default-export": false, 26 | "no-string-throw": true, 27 | "no-trailing-whitespace": true, 28 | "prefer-const": true, 29 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 30 | "typedef": [ 31 | true, 32 | "property-declaration" 33 | ], 34 | "typedef-whitespace": 35 | [ 36 | true, 37 | { 38 | "call-signature": "space", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | }, 44 | { 45 | "call-signature": "space", 46 | "index-signature": "nospace", 47 | "parameter": "nospace", 48 | "property-declaration": "nospace", 49 | "variable-declaration": "nospace" 50 | } 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-module", 56 | "check-operator", 57 | "check-separator" 58 | ], 59 | "align": [true, "arguments", "parameters", "statements"], 60 | "class-name": true, 61 | "member-access": true, 62 | "new-parens": true, 63 | "no-consecutive-blank-lines": true, 64 | "no-reference": true, 65 | "one-variable-per-declaration": [true, "ignore-for-loop"], 66 | "one-line": [ 67 | true, 68 | "check-catch", 69 | "check-finally", 70 | "check-else", 71 | "check-open-brace", 72 | "check-whitespace" 73 | ], 74 | "promise-function-async": true, 75 | "quotemark": [true, "single", "avoid-escape"], 76 | "semicolon": [true, "always"], 77 | "variable-name": [true, "ban-keywords"] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mudb", 3 | "version": "1.0.1", 4 | "description": "Real-time database for multiplayer games", 5 | "scripts": { 6 | "build": "tsc", 7 | "clean": "ls -F src | grep / | xargs rm -rf coverage *.d.ts *.js *.js.map", 8 | "coverage": "f(){ nyc tape -r ts-node/register src/${1:-\\*}/test/*.ts; }; f", 9 | "link": "npm link && npm run clean && npm run build", 10 | "release": "npm run test-all && npm run clean && npm run build && npm publish", 11 | "test": "f(){ tape -r ts-node/register src/${1:-\\*}/test/${2:-\\*}.ts; }; f", 12 | "test-all": "npm test socket/\\* && npm test", 13 | "watch": "tsc -w" 14 | }, 15 | "main": "index.js", 16 | "browser": { 17 | "rpc/http/client": "rpc/http/client-browser" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/mikolalysenko/mudb.git" 22 | }, 23 | "keywords": [ 24 | "mudb", 25 | "real-time", 26 | "multiplayer", 27 | "game", 28 | "network", 29 | "server", 30 | "client", 31 | "replicate", 32 | "schema", 33 | "binary", 34 | "buffer", 35 | "stream", 36 | "websocket", 37 | "tcp", 38 | "udp" 39 | ], 40 | "author": "Mikola Lysenko", 41 | "contributors": [ 42 | "He Diyi (https://github.com/hediyi/)" 43 | ], 44 | "license": "MIT", 45 | "dependencies": { 46 | "bufferutil": "4.0.2", 47 | "content-type": "1.0.4", 48 | "utf-8-validate": "5.0.3", 49 | "ws": "7.4.0" 50 | }, 51 | "devDependencies": { 52 | "@types/node": "^8.10.38", 53 | "@types/tape": "^4.2.32", 54 | "browserify": "^16.2.3", 55 | "nyc": "^15.0.0", 56 | "tape": "^4.9.1", 57 | "tape-run": "^4.0.0", 58 | "ts-node": "^7.0.1", 59 | "tsify": "^4.0.1", 60 | "tslint": "^5.11.0", 61 | "typescript": "^3.7.4", 62 | "typescript-tslint-plugin": "^0.1.0", 63 | "webworkify": "^1.5.0" 64 | }, 65 | "nyc": { 66 | "cache": true, 67 | "report-dir": "./coverage", 68 | "temp-dir": "./coverage/.nyc_output", 69 | "extension": [ 70 | ".ts" 71 | ], 72 | "reporter": [ 73 | "html" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { MuClient } from './client'; 2 | import { MuServer } from './server'; 3 | 4 | export { 5 | MuClient, 6 | MuServer, 7 | }; 8 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | export interface MuLogger { 2 | log:(mesg:string) => void; 3 | error:(mesg:string) => void; 4 | exception:(e:Error) => void; 5 | } 6 | 7 | export const MuDefaultLogger = { 8 | log: () => {}, 9 | error: (x:string) => console.log(`mudb error: ${x}`), 10 | exception: console.error, 11 | }; -------------------------------------------------------------------------------- /src/rda/constant.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from '../schema/schema'; 2 | import { MuVoid } from '../schema/void'; 3 | import { MuRDA, MuRDAStore, MuRDATypes } from './rda'; 4 | 5 | export class MuRDAConstantStore>> 6 | implements MuRDAStore { 7 | public value:MuRDATypes['state']; 8 | 9 | constructor (initial:MuRDATypes['state']) { 10 | this.value = initial; 11 | } 12 | 13 | public state (rda:RDA, out:MuRDATypes['state']) { 14 | return rda.stateSchema.assign(out, this.value); 15 | } 16 | 17 | public apply () { return false; } 18 | public inverse () { } 19 | 20 | public serialize (rda:RDA, out:MuRDATypes['serializedStore']) : MuRDATypes['serializedStore'] { 21 | return rda.storeSchema.assign(out, this.value); 22 | } 23 | 24 | public free (rda:RDA) { 25 | rda.stateSchema.free(this.value); 26 | } 27 | } 28 | 29 | type MuRDAConstantMeta = { 30 | type:'table'; 31 | table:{}; 32 | }; 33 | 34 | export class MuRDAConstant> 35 | implements MuRDA { 36 | public readonly stateSchema:StateSchema; 37 | public readonly actionSchema = new MuVoid(); 38 | public readonly storeSchema:StateSchema; 39 | public readonly actionMeta:MuRDAConstantMeta = { 40 | type:'table', 41 | table:{}, 42 | }; 43 | public readonly action = {}; 44 | 45 | public readonly emptyStore:MuRDAConstantStore; 46 | 47 | constructor (stateSchema:StateSchema) { 48 | this.stateSchema = stateSchema; 49 | this.storeSchema = stateSchema; 50 | this.emptyStore = new MuRDAConstantStore(stateSchema.identity); 51 | } 52 | 53 | public createStore (initialState:StateSchema['identity']) : MuRDAConstantStore { 54 | return new MuRDAConstantStore(this.stateSchema.clone(initialState)); 55 | } 56 | public parse (store:StateSchema['identity']) : MuRDAConstantStore { 57 | return new MuRDAConstantStore(this.stateSchema.clone(store)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/rda/index.ts: -------------------------------------------------------------------------------- 1 | import { MuRDA, MuRDAStore, MuRDATypes } from './rda'; 2 | import { MuRDAConstant, MuRDAConstantStore } from './constant'; 3 | import { MuRDARegister, MuRDARegisterStore } from './register'; 4 | import { MuRDAStruct, MuRDAStructStore } from './struct'; 5 | import { MuRDAMap, MuRDAMapStore } from './map'; 6 | import { MuRDAList, MuRDAListStore } from './list'; 7 | 8 | export { 9 | MuRDA, MuRDAStore, MuRDATypes, 10 | MuRDAConstant, MuRDAConstantStore, 11 | MuRDARegister, MuRDARegisterStore, 12 | MuRDAStruct, MuRDAStructStore, 13 | MuRDAMap, MuRDAMapStore, 14 | MuRDAList, MuRDAListStore, 15 | }; -------------------------------------------------------------------------------- /src/rda/rda.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from '../schema/schema'; 2 | 3 | // Typescript helpers 4 | export interface MuRDATypes> { 5 | // Type of an RDA's state schema and state object 6 | stateSchema:RDA['stateSchema']; 7 | state:this['stateSchema']['identity']; 8 | 9 | // Type of an RDA's action schema and action object 10 | actionSchema:RDA['actionSchema']; 11 | action:this['actionSchema']['identity']; 12 | 13 | // Store serialization schemas 14 | serializedStoreSchema:RDA['storeSchema']; 15 | serializedStore:this['serializedStoreSchema']['identity']; 16 | 17 | // Type of the store associated to an RDA 18 | store:RDA['emptyStore']; 19 | 20 | meta:RDA['actionMeta']; 21 | dispatcher:RDA['action']; 22 | } 23 | 24 | export interface MuRDA< 25 | StateSchema extends MuSchema, 26 | ActionSchema extends MuSchema, 27 | StoreSchema extends MuSchema, 28 | Meta extends MuRDABindableActionMeta> { 29 | readonly stateSchema:StateSchema; 30 | readonly actionSchema:ActionSchema; 31 | readonly storeSchema:StoreSchema; 32 | readonly emptyStore:MuRDAStore; 33 | 34 | // store constructors 35 | createStore(initialState:StateSchema['identity']) : this['emptyStore']; 36 | parse(inp:StoreSchema['identity']) : this['emptyStore']; 37 | 38 | // action constructor and reflection metadata 39 | actionMeta:Meta; 40 | action:any; 41 | } 42 | 43 | export interface MuRDAStore> { 44 | // computes a snapshot of the head state 45 | state(rda:RDA, out:MuRDATypes['state']) : MuRDATypes['state']; 46 | 47 | // apply takes an action and either appends it or moves it to the end of the queue 48 | apply(rda:RDA, action:MuRDATypes['action']) : boolean; 49 | 50 | // removes an action from the queue 51 | inverse(rda:RDA, action:MuRDATypes['action']) : MuRDATypes['action']; 52 | 53 | // serialize state of the store and all recorded actions into a MuSchema object 54 | serialize(rda:RDA, out:MuRDATypes['serializedStore']) : MuRDATypes['serializedStore']; 55 | 56 | // destroy store, clean up resources 57 | free(rda:RDA); 58 | } 59 | 60 | export type MuRDAActionMeta = 61 | { 62 | type:'unit'; 63 | } | 64 | { 65 | type:'partial'; 66 | action:MuRDAActionMeta; 67 | } | 68 | { 69 | type:'table'; 70 | table:{ 71 | [id:string]:MuRDAActionMeta; 72 | }; 73 | }; 74 | 75 | export type MuRDAAction = 76 | Meta extends { type:'unit' } 77 | ? (...args) => Action 78 | : Meta extends { type:'partial', action:MuRDAActionMeta } 79 | ? (...args) => MuRDAAction 80 | : Meta extends { type:'table', table:{ [id:string]:MuRDAActionMeta }} 81 | ? { 82 | [id in keyof Meta['table']]:MuRDAAction; 83 | } 84 | : never; 85 | 86 | export type MuRDABindableActionMeta = 87 | { 88 | type:'store'; 89 | action:MuRDAActionMeta; 90 | } | 91 | MuRDAActionMeta; 92 | 93 | export type MuRDABindableAction = 94 | Meta extends { type:'store', action:MuRDAActionMeta } 95 | ? (store) => MuRDAAction 96 | : Meta extends MuRDAActionMeta 97 | ? MuRDAAction 98 | : never; -------------------------------------------------------------------------------- /src/rda/register.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from '../schema/schema'; 2 | import { MuRDA, MuRDAStore, MuRDATypes } from './rda'; 3 | 4 | function identity (x:T) : T { return x; } 5 | 6 | export class MuRDARegisterStore> implements MuRDAStore { 7 | public value:MuRDATypes['state']; 8 | 9 | constructor (initial:MuRDATypes['state']) { this.value = initial; } 10 | 11 | public state (rda:RDA, out:MuRDATypes['state']) { return rda.stateSchema.assign(out, this.value); } 12 | public inverse (rda:RDA) { return rda.actionSchema.clone(this.value); } 13 | public free (rda:RDA) { rda.stateSchema.free(this.value); } 14 | 15 | public apply (rda:RDA, action:MuRDATypes['action']) { 16 | this.value = rda.stateSchema.assign(this.value, rda.constrain(action)); 17 | return true; 18 | } 19 | 20 | public serialize (rda:RDA, out:MuRDATypes['serializedStore']) : MuRDATypes['serializedStore'] { 21 | return rda.storeSchema.assign(out, this.value); 22 | } 23 | } 24 | 25 | type MuRDARegisterMeta = { 26 | type:'unit'; 27 | }; 28 | 29 | export class MuRDARegister> 30 | implements MuRDA { 31 | public readonly stateSchema:StateSchema; 32 | public readonly actionSchema:StateSchema; 33 | public readonly storeSchema:StateSchema; 34 | 35 | public readonly actionMeta:MuRDARegisterMeta = { type: 'unit' }; 36 | 37 | public action = (value:StateSchema['identity']) : StateSchema['identity'] => { 38 | return this.actionSchema.clone(this.constrain(value)); 39 | } 40 | 41 | public readonly emptyStore:MuRDARegisterStore; 42 | 43 | public constrain:(value:StateSchema['identity']) => StateSchema['identity']; 44 | 45 | constructor ( 46 | stateSchema:StateSchema, 47 | constrain?:(value:StateSchema['identity']) => StateSchema['identity']) { 48 | this.stateSchema = this.actionSchema = this.storeSchema = stateSchema; 49 | this.constrain = constrain || identity; 50 | this.emptyStore = new MuRDARegisterStore(stateSchema.identity); 51 | } 52 | 53 | public createStore (initialState:StateSchema['identity']) : MuRDARegisterStore { 54 | return new MuRDARegisterStore(this.stateSchema.clone(initialState)); 55 | } 56 | public parse (store:StateSchema['identity']) : MuRDARegisterStore { 57 | return new MuRDARegisterStore(this.stateSchema.clone(store)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/rda/test/constrain.ts: -------------------------------------------------------------------------------- 1 | import * as tape from 'tape'; 2 | import { MuFloat64, MuArray } from '../../schema'; 3 | import { MuRDARegister } from '../index'; 4 | 5 | tape('constrain - float', (t) => { 6 | const R = new MuRDARegister(new MuFloat64(), (x) => Math.max(0, Math.min(1, +x || 0))); 7 | t.equal(R.action(0.1), 0.1); 8 | t.equal(R.action(-0.1), 0); 9 | t.equal(R.action(1.1), 1); 10 | t.equal(R.action(NaN), 0); 11 | t.end(); 12 | }); 13 | 14 | tape('constrain - array', (t) => { 15 | const constrain = (a:number[]) => a.map((x) => Math.max(0, Math.min(1, +x || 0))); 16 | const R = new MuRDARegister(new MuArray(new MuFloat64(), Infinity), constrain); 17 | t.deepEqual(R.action([]), []); 18 | t.deepEqual(R.action([NaN, -0.1, 0, 0.1, 0.5, 1, 1.1]), [0, 0, 0, 0.1, 0.5, 1, 1]); 19 | t.end(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/rda/test/ids.ts: -------------------------------------------------------------------------------- 1 | import * as tape from 'tape'; 2 | import { Id, compareId, allocIds, initialIds, ID_MIN, ID_MAX, searchId } from '../_id'; 3 | 4 | function idRangeOk (pred:Id, succ:Id, range:Id[]) { 5 | for (let i = 0; i < range.length; ++i) { 6 | if (compareId(pred, range[i]) >= 0) { 7 | return false; 8 | } 9 | if (compareId(range[i], succ) >= 0) { 10 | return false; 11 | } 12 | if (i > 0 && compareId(range[i - 1], range[i]) >= 0) { 13 | return false; 14 | } 15 | } 16 | return true; 17 | } 18 | 19 | tape('id initialization', (t) => { 20 | const N = [ 21 | 1, 22 | 2, 23 | 10, 24 | 256, 25 | 65536, 26 | (1 << 20), 27 | ]; 28 | 29 | N.forEach((n) => { 30 | const ids = initialIds(n); 31 | t.ok( 32 | idRangeOk(ID_MIN, ID_MAX, ids), 33 | 'initialized valid ids'); 34 | t.equals( 35 | initialIds(n).join(), 36 | ids.join(), 37 | 'initialization deterministic'); 38 | }); 39 | 40 | t.end(); 41 | }); 42 | 43 | tape('id allocate', (t) => { 44 | function validateInsertion (ids:string[], index:number, count:number) { 45 | const pred = index > 0 ? ids[index - 1] : ID_MIN; 46 | const succ = index < ids.length ? ids[index] : ID_MAX; 47 | const alloc = allocIds(pred, succ, count); 48 | t.ok(idRangeOk(pred, succ, alloc), 'allocated ids ok'); 49 | ids.splice(index, 0, ...alloc); 50 | t.ok(idRangeOk(ID_MIN, ID_MAX, ids), 'inserted ids ok'); 51 | return ids; 52 | } 53 | 54 | for (let i = 0; i < 100; ++i) { 55 | let ids:Id[] = []; 56 | for (let j = 0; j < 100; ++j) { 57 | ids = validateInsertion(ids, Math.floor(Math.random() * (ids.length + 1)), 128); 58 | } 59 | } 60 | 61 | validateInsertion(['aaaa', 'baaa'], 1, (1 << 16)); 62 | 63 | t.end(); 64 | }); 65 | 66 | function searchBruteForce (ids:Id[], id:Id) { 67 | for (let i = 0; i < ids.length; ++i) { 68 | const d = compareId(ids[i], id); 69 | if (d >= 0) { 70 | return i; 71 | } 72 | } 73 | return ids.length; 74 | } 75 | 76 | tape('id search', (t) => { 77 | function testSearch (ids:Id[], id:Id, msg:string) { 78 | t.equals(searchId(ids, id), searchBruteForce(ids, id), msg); 79 | } 80 | 81 | function testList (ids:Id[]) { 82 | for (let i = 0; i < ids.length; ++i) { 83 | testSearch(ids, ids[i], `search element ${i}`); 84 | } 85 | testSearch(ids, ID_MIN, 'min'); 86 | testSearch(ids, ID_MAX, 'max'); 87 | 88 | const alloc = allocIds(ID_MIN, ID_MAX, 1000); 89 | for (let i = 0; i < alloc.length; ++i) { 90 | testSearch(ids, alloc[i], 'random'); 91 | } 92 | 93 | for (let i = 0; i <= ids.length; ++i) { 94 | allocIds( 95 | i > 0 ? ids[i - 1] : ID_MIN, 96 | i < ids.length ? ids[i] : ID_MAX, 97 | 16).forEach((id) => testSearch(ids, id, `range ${i}`)); 98 | } 99 | } 100 | 101 | testList(['@']); 102 | testList(['1', '2']); 103 | testList(['11', '33', '88']); 104 | testList(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); 105 | testList(initialIds(100)); 106 | 107 | t.end(); 108 | }); -------------------------------------------------------------------------------- /src/replica/index.ts: -------------------------------------------------------------------------------- 1 | import { MuReplicaServer } from './server'; 2 | import { MuReplicaClient } from './client'; 3 | 4 | export = { 5 | MuReplicaServer, 6 | MuReplicaClient, 7 | }; -------------------------------------------------------------------------------- /src/replica/schema.ts: -------------------------------------------------------------------------------- 1 | import { MuRDA } from '../rda/rda'; 2 | 3 | export function rdaProtocol>(rda:RDA) { 4 | return { 5 | client: { 6 | init: rda.storeSchema, 7 | squash: rda.stateSchema, 8 | apply: rda.actionSchema, 9 | }, 10 | server: { 11 | apply: rda.actionSchema, 12 | }, 13 | }; 14 | } 15 | 16 | export type RDAProtocol> = { 17 | client:{ 18 | init:RDA['storeSchema'], 19 | squash:RDA['stateSchema'], 20 | apply:RDA['actionSchema'], 21 | }, 22 | server:{ 23 | apply:RDA['actionSchema'], 24 | }, 25 | }; -------------------------------------------------------------------------------- /src/rpc/client.ts: -------------------------------------------------------------------------------- 1 | import { MuRPCProtocol, MuRPCSchemas, MuRPCClientTransport } from './protocol'; 2 | import { MuLogger, MuDefaultLogger } from '../logger'; 3 | 4 | export class MuRPCClient> { 5 | public api:{ 6 | [method in keyof Protocol['api']]: 7 | (arg:Protocol['api'][method]['arg']['identity']) => 8 | Promise; 9 | }; 10 | 11 | public schemas:MuRPCSchemas; 12 | public transport:MuRPCClientTransport; 13 | public logger:MuLogger; 14 | 15 | private _handleResponse = (response) => { 16 | const { type, data } = response; 17 | response.type = 'error'; 18 | response.data = ''; 19 | this.schemas.responseSchema.free(response); 20 | if (type === 'success') { 21 | return data.data; 22 | } else { 23 | this.logger.exception(data); 24 | throw data; 25 | } 26 | } 27 | 28 | private _createRPC (method:keyof Protocol['api']) { 29 | return (arg) => { 30 | const rpc = this.schemas.argSchema.alloc(); 31 | rpc.type = method; 32 | rpc.data = arg; 33 | return this.transport.send(this.schemas, rpc).then( 34 | this._handleResponse, 35 | (reason) => { 36 | this.logger.exception(reason); 37 | }); 38 | }; 39 | } 40 | 41 | constructor ( 42 | protocol:Protocol, 43 | transport:MuRPCClientTransport, 44 | logger?:MuLogger, 45 | ) { 46 | this.schemas = new MuRPCSchemas(protocol); 47 | this.transport = transport; 48 | this.logger = logger || MuDefaultLogger; 49 | const api = this.api = {}; 50 | const methods = Object.keys(protocol.api); 51 | for (let i = 0; i < methods.length; ++i) { 52 | const method = methods[i]; 53 | api[method] = this._createRPC(method); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/rpc/http/client-browser.ts: -------------------------------------------------------------------------------- 1 | import { MuRPCClientTransport, MuRPCProtocol, MuRPCSchemas } from '../protocol'; 2 | 3 | export class MuRPCHttpClientTransport implements MuRPCClientTransport { 4 | private _url:string; 5 | private _timeout:number; 6 | 7 | constructor(spec:{ 8 | url:string; 9 | timeout:number; 10 | }) { 11 | this._url = spec.url; 12 | this._timeout = spec.timeout; 13 | } 14 | 15 | public send> ( 16 | schemas:MuRPCSchemas, 17 | arg:MuRPCSchemas['argSchema']['identity'], 18 | ) { 19 | const xhr = new XMLHttpRequest(); 20 | xhr.open('POST', this._url + '/' + schemas.protocol.name, true); 21 | xhr.responseType = ''; 22 | if (this._timeout < Infinity && this._timeout) { 23 | xhr.timeout = this._timeout; 24 | } 25 | xhr.withCredentials = true; 26 | const body = JSON.stringify(schemas.argSchema.toJSON(arg)); 27 | return new Promise['responseSchema']['identity']>((resolve, reject) => { 28 | let completed = false; 29 | xhr.onreadystatechange = () => { 30 | if (completed) { 31 | return; 32 | } 33 | const readyState = xhr.readyState; 34 | if (readyState === 4) { 35 | completed = true; 36 | const responseText = xhr.responseText; 37 | try { 38 | let json:any; 39 | if (0 < responseText.length) { 40 | json = JSON.parse(responseText); 41 | } else { 42 | json = { 43 | type: 'error', 44 | data: 'empty response', 45 | }; 46 | } 47 | return resolve(schemas.responseSchema.fromJSON(json)); 48 | } catch (e) { 49 | return reject(e); 50 | } 51 | } 52 | }; 53 | xhr.onabort = () => { 54 | if (completed) { 55 | return; 56 | } 57 | reject(`request aborted [mudb/rpc]`); 58 | }; 59 | xhr.onerror = () => { 60 | if (completed) { 61 | return; 62 | } 63 | reject(`error during request [mudb/rpc]`); 64 | }; 65 | xhr.send(body); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/rpc/http/client-fetch.ts: -------------------------------------------------------------------------------- 1 | import { MuRPCClientTransport, MuRPCProtocol, MuRPCSchemas } from '../protocol'; 2 | 3 | export class MuRPCFetchClientTransport implements MuRPCClientTransport { 4 | private _url:string; 5 | constructor(spec:{ 6 | url:string; 7 | }) { 8 | this._url = spec.url; 9 | } 10 | 11 | public async send> ( 12 | schemas:MuRPCSchemas, 13 | arg:MuRPCSchemas['argSchema']['identity'], 14 | ) { 15 | const body = JSON.stringify(schemas.argSchema.toJSON(arg)); 16 | const response = await (await fetch(this._url + '/' + schemas.protocol.name, { 17 | method: 'POST', 18 | mode: 'same-origin', 19 | cache: 'no-cache', 20 | credentials: 'include', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | redirect: 'follow', 25 | body, 26 | })).json(); 27 | return schemas.responseSchema.fromJSON(response); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/rpc/http/client.ts: -------------------------------------------------------------------------------- 1 | import { MuRPCClientTransport, MuRPCProtocol, MuRPCSchemas } from '../protocol'; 2 | import * as http from 'http'; 3 | 4 | export class MuRPCHttpClientTransport implements MuRPCClientTransport { 5 | private _url:string; 6 | private _cookies:{ 7 | [cookie:string]:string; 8 | } = {}; 9 | private _headers; 10 | 11 | constructor (spec:{ 12 | url:string; 13 | timeout:number; 14 | headers?:object; 15 | }) { 16 | this._url = spec.url; 17 | this._headers = spec.headers || {}; 18 | } 19 | 20 | public send> ( 21 | schemas:MuRPCSchemas, 22 | arg:MuRPCSchemas['argSchema']['identity'], 23 | ) { 24 | const argBuffer = Buffer.from(JSON.stringify(schemas.argSchema.toJSON(arg)), 'utf8'); 25 | return new Promise['responseSchema']['identity']>((resolve, reject) => { 26 | let completed = false; 27 | const chunks:string[] = []; 28 | 29 | function done (error:any, payload?:string) { 30 | if (completed) { 31 | return; 32 | } 33 | completed = true; 34 | chunks.length = 0; 35 | if (error) { 36 | return reject(error); 37 | } else if (!payload) { 38 | return reject('unspecified error'); 39 | } 40 | try { 41 | let json:any = void 0; 42 | if (payload.length > 0) { 43 | json = JSON.parse(payload); 44 | } 45 | return resolve(schemas.responseSchema.fromJSON(json)); 46 | } catch (e) { 47 | return reject(e); 48 | } 49 | } 50 | 51 | const url = new URL(this._url + '/' + schemas.protocol.name); 52 | const req = http.request({ 53 | hostname: url.hostname, 54 | port: url.port, 55 | path: url.pathname, 56 | method: 'POST', 57 | headers: { 58 | 'Content-Type': 'application/x-www-form-urlencoded', 59 | 'Content-Length': argBuffer.length, 60 | 'Cookie': Object.keys(this._cookies).map((cookie) => `${cookie}=${encodeURIComponent(this._cookies[cookie])}`).join('; '), 61 | ...this._headers, 62 | }, 63 | }, (res) => { 64 | const cookie = res.headers['set-cookie']; 65 | if (cookie) { 66 | for (let i = 0; i < cookie.length; ++i) { 67 | const parts = cookie[i].split('; '); 68 | if (parts.length < 1) { 69 | continue; 70 | } 71 | const tokens = parts[0].split('='); 72 | this._cookies[tokens[0]] = decodeURIComponent(tokens[1]); 73 | } 74 | } 75 | res.setEncoding('utf8'); 76 | res.on('data', (chunk) => { 77 | if (completed) { 78 | return; 79 | } else if (typeof chunk === 'string') { 80 | chunks.push(chunk); 81 | } else { 82 | chunks.push(chunk.toString('utf8')); 83 | } 84 | }); 85 | res.on('end', () => { 86 | if (completed) { 87 | return; 88 | } 89 | done(void 0, chunks.join('')); 90 | }); 91 | }); 92 | req.on('error', (err) => done(err)); 93 | req.end(argBuffer); 94 | }); 95 | } 96 | } -------------------------------------------------------------------------------- /src/rpc/intercept.ts: -------------------------------------------------------------------------------- 1 | import { MuLogger } from '../logger'; 2 | import { MuRPCClient } from './client'; 3 | import { MuRPCClientTransport, MuRPCConnection, MuRPCProtocol, MuRPCSchemas, MuRPCServerTransport } from './protocol'; 4 | import { MuRPCServer } from './server'; 5 | 6 | // Helper class to intercept RPC methods between a client and server 7 | export class MuRPCIntercept, Connection extends MuRPCConnection> { 8 | public protocol:Protocol; 9 | public logger:MuLogger; 10 | public remote:MuRPCClient; 11 | public schemas:MuRPCSchemas; 12 | 13 | private _transport:MuRPCServerTransport; 14 | private _server?:MuRPCServer; 15 | 16 | constructor (spec:{ 17 | protocol:Protocol, 18 | client:MuRPCClientTransport, 19 | server:MuRPCServerTransport, 20 | logger:MuLogger, 21 | }) { 22 | this.protocol = spec.protocol; 23 | this.logger = spec.logger; 24 | this._transport = spec.server; 25 | this.remote = new MuRPCClient(spec.protocol, spec.client, spec.logger); 26 | this.schemas = this.remote.schemas; 27 | } 28 | 29 | public configure(spec:{ 30 | authorize?:(conn:Connection) => Promise, 31 | handlers:Partial<{ 32 | [method in keyof Protocol['api']]: 33 | (conn:Connection, arg:Protocol['api'][method]['arg']['identity']) => 34 | Promise 35 | }>, 36 | }) { 37 | const handlers:any = {}; 38 | const methods = Object.keys(this.protocol.api); 39 | for (let i = 0; i < methods.length; ++i) { 40 | if (methods[i] in spec.handlers) { 41 | handlers[methods[i]] = spec.handlers[methods[i]]; 42 | } else { 43 | handlers[methods[i]] = (conn, arg) => this.remote.api[methods[i]](arg); 44 | } 45 | } 46 | this._server = new MuRPCServer({ 47 | protocol: this.protocol, 48 | transport: this._transport, 49 | authorize: spec.authorize || (() => Promise.resolve(true)), 50 | handlers, 51 | logger: this.logger, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rpc/local/index.ts: -------------------------------------------------------------------------------- 1 | import { MuRPCSchemas, MuRPCClientTransport, MuRPCServerTransport, MuRPCProtocol, MuRPCConnection } from '../protocol'; 2 | 3 | export class MuRPCLocalClient> 4 | implements MuRPCClientTransport, MuRPCConnection { 5 | constructor ( 6 | public auth:string, 7 | private _handlers:{ 8 | [name:string]:(auth:MuRPCLocalClient, rpc:any) => Promise; 9 | }, 10 | ) {} 11 | 12 | public setAuth (auth:string) { 13 | this.auth = auth; 14 | } 15 | 16 | public async send ( 17 | schemas:MuRPCSchemas, 18 | args:MuRPCSchemas['argSchema']['identity']) { 19 | const handler = this._handlers[schemas.protocol.name]; 20 | if (!handler) { 21 | throw new Error('server not registered'); 22 | } 23 | const json = await handler(this, schemas.argSchema.toJSON(args)); 24 | return schemas.responseSchema.fromJSON(json); 25 | } 26 | } 27 | 28 | export class MuRPCLocalTransport implements MuRPCServerTransport> { 29 | private _handlers:{ 30 | [name:string]:(auth:MuRPCLocalClient, rpc:any) => Promise; 31 | } = {}; 32 | 33 | public client> (auth:string) { 34 | return new MuRPCLocalClient(auth, this._handlers); 35 | } 36 | 37 | public listen> ( 38 | schemas:MuRPCSchemas, 39 | auth:(conn:MuRPCLocalClient) => Promise, 40 | recv:( 41 | conn:MuRPCLocalClient, 42 | rpc:MuRPCSchemas['argSchema']['identity'], 43 | response:MuRPCSchemas['responseSchema']['identity']) => Promise) { 44 | this._handlers[schemas.protocol.name] = async (client, json) => { 45 | const response = schemas.responseSchema.alloc(); 46 | try { 47 | if (!await auth(client)) { 48 | throw new Error('unauthorized access'); 49 | } 50 | const parsed = schemas.argSchema.fromJSON(json); 51 | await recv(client, parsed, response); 52 | schemas.argSchema.free(parsed); 53 | } catch (e) { 54 | response.type = 'error'; 55 | if (e instanceof Error && e.stack) { 56 | response.data = e.stack; 57 | } else { 58 | response.data = '' + e ; 59 | } 60 | } 61 | const result = schemas.responseSchema.toJSON(response); 62 | schemas.responseSchema.free(response); 63 | return result; 64 | }; 65 | return {}; 66 | } 67 | } -------------------------------------------------------------------------------- /src/rpc/protocol.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from '../schema/schema'; 2 | import { MuUnion, MuVarint, MuUTF8 } from '../schema'; 3 | 4 | export type MuRPCTableEntry< 5 | ArgsSchema extends MuSchema, 6 | ReturnSchema extends MuSchema> = { 7 | arg:ArgsSchema; 8 | ret:ReturnSchema; 9 | }; 10 | 11 | export type MuRPCTable = { 12 | [method:string]:MuRPCTableEntry; 13 | }; 14 | 15 | export type MuRPCProtocol = { 16 | name:string; 17 | api:RPCTable; 18 | }; 19 | 20 | export class MuRPCSchemas> { 21 | public errorSchema = new MuUTF8(); 22 | public tokenSchema = new MuVarint(); 23 | public argSchema:MuUnion<{ 24 | [method in keyof Protocol['api']]:Protocol['api']['arg']; 25 | }>; 26 | public retSchema:MuUnion<{ 27 | [method in keyof Protocol['api']]:Protocol['api']['ret']; 28 | }>; 29 | public responseSchema:MuUnion<{ 30 | success:MuRPCSchemas['retSchema']; 31 | error:MuRPCSchemas['errorSchema']; 32 | }>; 33 | public error (message:string) { 34 | const result = this.responseSchema.alloc(); 35 | result.type = 'error'; 36 | result.data = message; 37 | return result; 38 | } 39 | 40 | constructor( 41 | public protocol:Protocol, 42 | ) { 43 | const argTable:any = {}; 44 | const retTable:any = {}; 45 | const methods = Object.keys(protocol.api); 46 | for (let i = 0; i < methods.length; ++i) { 47 | const m = methods[i]; 48 | const s = protocol.api[m]; 49 | argTable[m] = s.arg; 50 | retTable[m] = s.ret; 51 | } 52 | this.argSchema = new MuUnion(argTable); 53 | this.retSchema = new MuUnion(retTable); 54 | this.responseSchema = new MuUnion({ 55 | success: this.retSchema, 56 | error: this.errorSchema, 57 | }); 58 | } 59 | } 60 | 61 | export interface MuRPCClientTransport> { 62 | send:( 63 | schema:MuRPCSchemas, 64 | rpc:MuRPCSchemas['argSchema']['identity']) => 65 | Promise['responseSchema']['identity']>; 66 | } 67 | 68 | export interface MuRPCConnection { 69 | auth:string; 70 | setAuth:(auth:string) => void; 71 | } 72 | 73 | export interface MuRPCServerTransport, Connection extends MuRPCConnection> { 74 | listen:( 75 | schemas:MuRPCSchemas, 76 | authorize:(connection:Connection) => Promise, 77 | recv:( 78 | connection:Connection, 79 | rpc:MuRPCSchemas['argSchema']['identity'], 80 | response:MuRPCSchemas['responseSchema']['identity']) => Promise) => void; 81 | } 82 | -------------------------------------------------------------------------------- /src/rpc/server.ts: -------------------------------------------------------------------------------- 1 | import { MuRPCProtocol, MuRPCSchemas, MuRPCServerTransport, MuRPCConnection } from './protocol'; 2 | import { MuLogger } from '../logger'; 3 | 4 | export class MuRPCServer, Connection extends MuRPCConnection> { 5 | public schemas:MuRPCSchemas; 6 | public transport:MuRPCServerTransport; 7 | 8 | constructor (spec:{ 9 | protocol:Protocol, 10 | transport:MuRPCServerTransport, 11 | authorize:(conn:Connection) => Promise, 12 | handlers:{ 13 | [method in keyof Protocol['api']]: 14 | (conn:Connection, arg:Protocol['api'][method]['arg']['identity'], ret:Protocol['api'][method]['ret']['identity']) => 15 | Promise; 16 | }, 17 | logger?:MuLogger, 18 | }) { 19 | const logger = spec.logger; 20 | const schemas = this.schemas = new MuRPCSchemas(spec.protocol); 21 | this.transport = spec.transport; 22 | this.transport.listen( 23 | schemas, 24 | spec.authorize, 25 | async (conn, arg, response) => { 26 | try { 27 | const method = arg.type; 28 | const handler = spec.handlers[method]; 29 | if (!handler) { 30 | logger && logger.error(`invalid rpc method: ${method}`); 31 | response.type = 'error'; 32 | response.data = `invalid rpc method: ${method}`; 33 | } else { 34 | const retSchema = schemas.protocol.api[method].ret; 35 | if (handler.length === 3) { 36 | const ret = retSchema.alloc(); 37 | const actualRet = await handler(conn, arg.data, ret); 38 | response.type = 'success'; 39 | const retInfo = response.data = schemas.retSchema.alloc(); 40 | retInfo.type = method; 41 | if (ret === actualRet) { 42 | retInfo.data = ret; 43 | } else { 44 | logger && logger.log(`warning, handler for ${method} did not use storage for return type`); 45 | retSchema.free(ret); 46 | retInfo.data = actualRet; 47 | } 48 | } else { 49 | // if user doesn't take storage as an argument, then we just leak the response reference 50 | const ret = await handler(conn, arg.data, undefined); 51 | response.type = 'success'; 52 | const retInfo = response.data = schemas.retSchema.alloc(); 53 | retInfo.type = method; 54 | retInfo.data = retSchema.clone(ret); 55 | } 56 | } 57 | } catch (e) { 58 | logger && logger.error(e); 59 | response.type = 'error'; 60 | if (e instanceof Error && e.message) { 61 | response.data = e.message; 62 | } else { 63 | response.data = e; 64 | } 65 | } 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/rpc/test/basic.ts: -------------------------------------------------------------------------------- 1 | import * as tape from 'tape'; 2 | import { MuRPCLocalTransport } from '../local'; 3 | import { MuUTF8, MuStruct, MuVarint, MuVoid } from '../../schema'; 4 | import { MuRPCServer } from '../server'; 5 | import { MuRPCClient } from '../client'; 6 | 7 | tape('basic rpc', async (t) => { 8 | const protocol = { 9 | name: 'test rpc', 10 | api: { 11 | hello:{ 12 | arg: new MuUTF8(), 13 | ret: new MuUTF8(), 14 | }, 15 | fib:{ 16 | arg: new MuStruct({ 17 | a: new MuVarint(), 18 | b: new MuVarint(), 19 | }), 20 | ret: new MuStruct({ 21 | a: new MuVarint(), 22 | b: new MuVarint(), 23 | }), 24 | }, 25 | brokenRoute: { 26 | arg: new MuVoid(), 27 | ret: new MuVoid(), 28 | }, 29 | logout: { 30 | arg: new MuVoid(), 31 | ret: new MuVoid(), 32 | }, 33 | }, 34 | }; 35 | 36 | const transport = new MuRPCLocalTransport(); 37 | 38 | const server = new MuRPCServer({ 39 | protocol, 40 | transport, 41 | authorize: async ({ auth }) => auth !== 'bad guy', 42 | handlers: { 43 | hello: async ({ auth }, arg) => { 44 | if (auth === 'admin') { 45 | return 'administrator'; 46 | } 47 | return 'hello ' + arg; 48 | }, 49 | fib: async (conn, { a, b }, ret) => { 50 | ret.a = a + b; 51 | ret.b = a; 52 | return ret; 53 | }, 54 | brokenRoute: async () => { 55 | throw new Error('not implemented'); 56 | }, 57 | logout: async (conn) => { 58 | conn.setAuth(''); 59 | }, 60 | }, 61 | }); 62 | 63 | const client = new MuRPCClient( 64 | protocol, 65 | transport.client('user')); 66 | t.equals(await client.api.hello('world'), 'hello world', 'hello world'); 67 | t.same(await client.api.fib({ a: 2, b: 1 }), { a: 3, b: 2 }, 'fibonacci'); 68 | 69 | try { 70 | await client.api.brokenRoute(); 71 | t.fail('should throw'); 72 | } catch (e) { 73 | t.pass(`throws ${e}`); 74 | } 75 | 76 | const badClient = new MuRPCClient( 77 | protocol, 78 | transport.client('bad guy')); 79 | try { 80 | await badClient.api.hello('i am a jerk'); 81 | t.fail('should throw'); 82 | } catch (e) { 83 | t.pass('bad api throws when called'); 84 | } 85 | 86 | const adminClient = new MuRPCClient( 87 | protocol, 88 | transport.client('admin')); 89 | t.equals(await adminClient.api.hello('guest'), 'administrator', 'admin auth ok'); 90 | await adminClient.api.logout(); 91 | t.equals(await adminClient.api.hello('guest'), 'hello guest', 'log out ok'); 92 | 93 | t.end(); 94 | }); -------------------------------------------------------------------------------- /src/rpc/test/http.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as tape from 'tape'; 3 | import { MuRPCHttpServerTransport } from '../http/server'; 4 | import { MuUTF8, MuVoid, MuStruct, MuFloat64 } from '../../schema'; 5 | import { MuRPCServer } from '../server'; 6 | import { findPortAsync } from '../../util/port'; 7 | import { MuRPCClient } from '../client'; 8 | import { MuRPCHttpClientTransport } from '../http/client'; 9 | 10 | tape('http server', async (t) => { 11 | const protocol = { 12 | name: 'test', 13 | api: { 14 | login:{ 15 | arg: new MuUTF8(), 16 | ret: new MuVoid(), 17 | }, 18 | logout:{ 19 | arg: new MuVoid(), 20 | ret: new MuVoid(), 21 | }, 22 | hello: { 23 | arg: new MuVoid(), 24 | ret: new MuUTF8(), 25 | }, 26 | add: { 27 | arg: new MuStruct({ 28 | a: new MuFloat64(), 29 | b: new MuFloat64(), 30 | }), 31 | ret: new MuFloat64(), 32 | }, 33 | }, 34 | }; 35 | 36 | const transport = new MuRPCHttpServerTransport({ 37 | route: 'api', 38 | byteLimit: 1 << 20, 39 | cookie: 'auth', 40 | }); 41 | 42 | const httpServer = http.createServer(async (req, res) => { 43 | if (!transport.handler(req, res)) { 44 | t.fail('unhandled route'); 45 | res.statusCode = 404; 46 | res.end(); 47 | } 48 | }); 49 | 50 | const port = await findPortAsync(); 51 | httpServer.listen(port); 52 | 53 | const server = new MuRPCServer({ 54 | protocol, 55 | transport, 56 | authorize: async (connection) => { 57 | if (connection.auth === 'bad') { 58 | return false; 59 | } 60 | return true; 61 | }, 62 | handlers: { 63 | login: async (conn, handle) => { 64 | conn.setAuth(handle); 65 | }, 66 | logout: async (conn) => { 67 | conn.setAuth(''); 68 | }, 69 | hello: async (conn) => { 70 | if (conn.auth) { 71 | return `hello ${conn.auth}`; 72 | } 73 | return `hello guest`; 74 | }, 75 | add: async (conn, { a, b }) => { 76 | return a + b; 77 | }, 78 | }, 79 | }); 80 | 81 | const client = new MuRPCClient(protocol, new MuRPCHttpClientTransport({ 82 | url: `http://127.0.0.1:${port}/api`, 83 | timeout: Infinity, 84 | })); 85 | 86 | t.equals(await client.api.hello(), `hello guest`); 87 | await client.api.login('user'); 88 | t.equals(await client.api.hello(), `hello user`); 89 | 90 | httpServer.close(); 91 | 92 | t.end(); 93 | }); 94 | -------------------------------------------------------------------------------- /src/scheduler/README.md: -------------------------------------------------------------------------------- 1 | # scheduler 2 | for unifying the usage of schedulers to make it possible to inject mocks 3 | 4 | ## example 5 | 6 | ```ts 7 | import { MuServer, MuClient } from 'mudb' 8 | import { createLocalSocketServer, createLocalSocket } from 'mudb/socket/local' 9 | import { MuMockScheduler } from 'mudb/scheduler/mock' 10 | 11 | const mockScheduler = new MuMockScheduler() 12 | const socketServer = createLocalSocketServer({ 13 | scheduler: mockScheduler, 14 | }) 15 | const server = new MuServer(socketServer) 16 | 17 | const socket = createLocalSocket({ 18 | sessionId: Math.random().toString(16).substring(2), 19 | server: socketServer, 20 | scheduler: mockScheduler, 21 | }) 22 | const client = new MuClient(socket) 23 | 24 | // trigger one scheduled event at a time in order 25 | while (mockScheduler.poll()) { } 26 | ``` 27 | 28 | ## API 29 | * [`MuScheduler`](#muscheduler) 30 | * [`MuSystemScheduler`](#musystemscheduler) 31 | * [`MuMockScheduler`](#mumockscheduler) 32 | 33 | ### `MuScheduler` 34 | the interface 35 | 36 | #### methods 37 | 38 | ```ts 39 | setTimeout( 40 | callback:(...args:any[]) => void, 41 | delay:number, 42 | ...args:any[], 43 | ) : any 44 | ``` 45 | 46 | ```ts 47 | clearTimeout(handle:any) : void; 48 | ``` 49 | 50 | ```ts 51 | setInterval( 52 | callback:(...args:any[]) => void, 53 | delay:number, 54 | ...args:any[], 55 | ) : any; 56 | ``` 57 | 58 | ```ts 59 | clearInterval(handle:any) : void; 60 | ``` 61 | 62 | ```ts 63 | requestAnimationFrame(callback:(time:number) => void) : number 64 | ``` 65 | 66 | ```ts 67 | cancelAnimationFrame(handle:number) : void 68 | ``` 69 | 70 | ```ts 71 | requestIdleCallback( 72 | callback:(deadline:{ 73 | didTimeout:boolean; 74 | timeRemaining:() => number; 75 | }) => void, 76 | options?:{ timeout:number }, 77 | ) : any 78 | ``` 79 | 80 | ```ts 81 | cancelIdleCallback(handle:any) : void 82 | ``` 83 | 84 | ```ts 85 | nextTick(callback:(...args:any[]) => void) : void 86 | ``` 87 | for `process.nextTick()` 88 | 89 | ```ts 90 | now() : number 91 | ``` 92 | for `performance.now()` 93 | 94 | --- 95 | 96 | ### `MuSystemScheduler` 97 | a singleton of type [`MuScheduler`](#muscheduler) 98 | 99 | This is the default value of all the `scheduler` parameters. It provides indirections to all the native schedulers, uses polyfills as necessary. 100 | 101 | --- 102 | 103 | ### `MuMockScheduler` 104 | class `MuMockScheduler` implements [`MuScheduler`](#muscheduler) 105 | 106 | ```ts 107 | import { MuMockScheduler } from 'mudb/scheduler/mock' 108 | 109 | new MuMockScheduler() 110 | ``` 111 | 112 | It provides mocks to all the schedulers, which share the same event queue that can be polled, to give you some control over "the passage of time". 113 | 114 | #### methods 115 | 116 | ```ts 117 | poll() : boolean 118 | ``` 119 | returns false if the event queue is empty, otherwise pops one event and returns true 120 | -------------------------------------------------------------------------------- /src/scheduler/index.ts: -------------------------------------------------------------------------------- 1 | import { MuScheduler } from './scheduler'; 2 | import { MuSystemScheduler } from './system'; 3 | import { MuMockScheduler } from './mock'; 4 | 5 | export { 6 | MuScheduler, 7 | MuSystemScheduler, 8 | MuMockScheduler, 9 | }; 10 | -------------------------------------------------------------------------------- /src/scheduler/mock.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuScheduler, 3 | MuRequestAnimationFrame, 4 | MuCancelAnimationFrame, 5 | MuRequestIdleCallback, 6 | MuCancelIdleCallback, 7 | MuProcessNextTick, 8 | } from './scheduler'; 9 | import { NIL, PQEvent, pop, createNode, merge, decreaseKey } from './pq'; 10 | import { perfNow } from './perf-now'; 11 | 12 | const frameDuration = 1000 / 60; 13 | 14 | export class MuMockScheduler implements MuScheduler { 15 | private _eventQueue = NIL; 16 | private _timeoutCounter:number = 0; 17 | private _mockMSCounter = 0; 18 | private _idToEvent:{ [id:number]:PQEvent } = {}; 19 | 20 | public now () { 21 | return this._mockMSCounter; 22 | } 23 | 24 | public poll () : boolean { 25 | if (this._eventQueue === NIL) { 26 | return false; 27 | } 28 | 29 | this._mockMSCounter = this._eventQueue.time; 30 | const event = this._eventQueue.event; 31 | delete this._idToEvent[this._eventQueue.id]; 32 | this._eventQueue = pop(this._eventQueue); 33 | event(); 34 | 35 | return true; 36 | } 37 | 38 | public setTimeout = (callback:() => void, ms:number) : number => { 39 | const id = this._timeoutCounter++; 40 | const time = 1 + this._mockMSCounter + Math.max(ms, 0); 41 | const node = createNode(id, time, callback); 42 | this._idToEvent[id] = node; 43 | this._eventQueue = merge(node, this._eventQueue); 44 | return id; 45 | } 46 | 47 | public clearTimeout = (id:number) => { 48 | const node = this._idToEvent[id]; 49 | if (node) { 50 | this._eventQueue = decreaseKey(this._eventQueue, node, -Infinity); 51 | this._eventQueue = pop(this._eventQueue); 52 | delete this._idToEvent[id]; 53 | } 54 | } 55 | 56 | public setInterval = (callback:() => void, ms:number) : number => { 57 | const id = this._timeoutCounter++; 58 | const self = this; 59 | 60 | function insertNode () { 61 | const time = 1 + self._mockMSCounter + Math.max(ms, 0); 62 | const node = createNode(id, time, event); 63 | self._idToEvent[id] = node; 64 | self._eventQueue = merge(node, self._eventQueue); 65 | } 66 | 67 | function event () { 68 | insertNode(); 69 | callback(); 70 | } 71 | 72 | insertNode(); 73 | 74 | return id; 75 | } 76 | 77 | public clearInterval = this.clearTimeout; 78 | 79 | private _rAFLast = 0; 80 | public requestAnimationFrame:MuRequestAnimationFrame = (callback) => { 81 | const now_ = perfNow(); 82 | const timeout = Math.max(0, frameDuration - (now_ - this._rAFLast)); 83 | const then = this._rAFLast = now_ + timeout; 84 | 85 | return this.setTimeout(() => callback(then), Math.round(timeout)); 86 | } 87 | 88 | public cancelAnimationFrame:MuCancelAnimationFrame = this.clearTimeout; 89 | 90 | public requestIdleCallback:MuRequestIdleCallback = (callback, options?) => { 91 | const timeout = options ? options.timeout : 1; 92 | return this.setTimeout(() => { 93 | const start = perfNow(); 94 | callback({ 95 | didTimeout: false, 96 | timeRemaining: () => Math.max(0, 50 - (perfNow() - start)), 97 | }); 98 | }, timeout); 99 | } 100 | 101 | public cancelIdleCallback:MuCancelIdleCallback = this.clearTimeout; 102 | 103 | public nextTick:MuProcessNextTick = (callback) => { 104 | this.setTimeout(callback, 0); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/scheduler/perf-now.ts: -------------------------------------------------------------------------------- 1 | export let perfNow:() => number; 2 | 3 | if (typeof performance !== 'undefined' && typeof performance.now === 'function') { 4 | perfNow = () => performance.now(); 5 | } else if (typeof process !== 'undefined' && typeof process.hrtime === 'function') { 6 | perfNow = (() => { 7 | function nanoSeconds () { 8 | const hrt = process.hrtime(); 9 | return hrt[0] * 1e9 + hrt[1]; 10 | } 11 | const loadTime = nanoSeconds() - process.uptime() * 1e9; 12 | return () => (nanoSeconds() - loadTime) / 1e6; 13 | })(); 14 | } else if (typeof Date.now === 'function') { 15 | perfNow = (() => { 16 | const loadTime = Date.now(); 17 | return () => Date.now() - loadTime; 18 | })(); 19 | } else { 20 | perfNow = (() => { 21 | const loadTime = new Date().getTime(); 22 | return () => new Date().getTime() - loadTime; 23 | })(); 24 | } 25 | -------------------------------------------------------------------------------- /src/scheduler/pq.ts: -------------------------------------------------------------------------------- 1 | export class PQEvent { 2 | public id:number; 3 | public time:number; 4 | public event:() => void; 5 | public parent:PQEvent; 6 | public left:PQEvent; 7 | public right:PQEvent; 8 | 9 | constructor (id:number, time:number, event:() => void, parent:PQEvent, left:PQEvent, right:PQEvent) { 10 | this.id = id; 11 | this.time = time; 12 | this.event = event; 13 | this.parent = parent; 14 | this.left = left; 15 | this.right = right; 16 | } 17 | } 18 | 19 | export const NIL = new PQEvent(-1, -Infinity, () => {}, null, null, null); 20 | NIL.parent = NIL.left = NIL.right = NIL; 21 | 22 | function link (a:PQEvent, b:PQEvent) { 23 | b.right = a.left; 24 | a.left.parent = b; 25 | a.left = b; 26 | b.parent = a; 27 | a.right = NIL; 28 | return a; 29 | } 30 | 31 | export function merge (a:PQEvent, b:PQEvent) { 32 | if (a === NIL) { 33 | return b; 34 | } else if (b === NIL) { 35 | return a; 36 | } else if (a.time < b.time) { 37 | return link(a, b); 38 | } else { 39 | return link(b, a); 40 | } 41 | } 42 | 43 | export function pop (root:PQEvent) { 44 | let p = root.left; 45 | root.left = NIL; 46 | root = p; 47 | while (true) { 48 | let q = root.right; 49 | if (q === NIL) { 50 | break; 51 | } 52 | p = root; 53 | let r = q.right; 54 | let s = merge(p, q); 55 | root = s; 56 | while (true) { 57 | p = r; 58 | q = r.right; 59 | if (q === NIL) { 60 | break; 61 | } 62 | r = q.right; 63 | s = s.right = merge(p, q); 64 | } 65 | s.right = NIL; 66 | if (p !== NIL) { 67 | p.right = root; 68 | root = p; 69 | } 70 | } 71 | root.parent = NIL; 72 | return root; 73 | } 74 | 75 | export function decreaseKey (root:PQEvent, p:PQEvent, time:number) { 76 | p.time = time; 77 | const q = p.parent; 78 | if (q.time < p.time) { 79 | return root; 80 | } 81 | const r = p.right; 82 | r.parent = q; 83 | if (q.left === p) { 84 | q.left = r; 85 | } else { 86 | q.right = r; 87 | } 88 | if (root.time <= p.time) { 89 | const l = root.left; 90 | l.parent = p; 91 | p.right = l; 92 | root.left = p; 93 | p.parent = root; 94 | return root; 95 | } else { 96 | const l = p.left; 97 | root.right = l; 98 | l.parent = root; 99 | p.left = root; 100 | root.parent = p; 101 | p.right = p.parent = NIL; 102 | return p; 103 | } 104 | } 105 | 106 | export function createNode (id:number, time:number, event:() => void) { 107 | return new PQEvent(id, time, event, NIL, NIL, NIL); 108 | } 109 | -------------------------------------------------------------------------------- /src/scheduler/scheduler.ts: -------------------------------------------------------------------------------- 1 | export type MuTimer = NodeJS.Timer | number; 2 | 3 | export type MuRequestAnimationFrame = (callback:(time:number) => void) => number; 4 | 5 | export type MuCancelAnimationFrame = (handle:number) => void; 6 | 7 | export interface MuIdleDeadline { 8 | readonly didTimeout:boolean; 9 | timeRemaining:() => number; 10 | } 11 | 12 | export type MuRequestIdleCallback = ( 13 | callback:(deadline:MuIdleDeadline) => void, 14 | options?:{ timeout:number }, 15 | ) => MuTimer; 16 | 17 | export type MuCancelIdleCallback = (handle:any) => void; 18 | 19 | export type MuProcessNextTick = (callback:(...args:any[]) => void) => void; 20 | 21 | export interface MuScheduler { 22 | now:() => number; 23 | setTimeout:(callback:(...args:any[]) => void, ms:number, ...args:any[]) => MuTimer; 24 | clearTimeout:(handle:any) => void; 25 | setInterval:(callback:(...args:any[]) => void, ms:number, ...args:any[]) => MuTimer; 26 | clearInterval:(handle:any) => void; 27 | requestAnimationFrame:MuRequestAnimationFrame; 28 | cancelAnimationFrame:MuCancelAnimationFrame; 29 | requestIdleCallback:MuRequestIdleCallback; 30 | cancelIdleCallback:MuCancelIdleCallback; 31 | nextTick:MuProcessNextTick; 32 | } 33 | -------------------------------------------------------------------------------- /src/scheduler/test/system.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { MuSystemScheduler } from '../system'; 3 | 4 | test('cAF', (t) => { 5 | t.plan(3); 6 | 7 | function cb1 () { cb1['called'] = true; } 8 | function cb2 () { cb2['called'] = true; } 9 | function cb3 () { cb3['called'] = true; } 10 | 11 | MuSystemScheduler.requestAnimationFrame(cb1); 12 | const handle = MuSystemScheduler.requestAnimationFrame(cb2); 13 | MuSystemScheduler.requestAnimationFrame(cb3); 14 | MuSystemScheduler.cancelAnimationFrame(handle); 15 | 16 | MuSystemScheduler.requestAnimationFrame(() => { 17 | t.true(cb1['called'], 'cb1 was called'); 18 | t.notOk(cb2['called'], 'cb2 was not called'); 19 | t.true(cb3['called'], 'cb3 was called'); 20 | t.end(); 21 | }); 22 | }); 23 | 24 | test('rAF does not eat errors', (t) => { 25 | if (typeof process !== 'undefined') { 26 | process.on('uncaughtException', () => { 27 | process.removeAllListeners('uncaughtException'); 28 | t.pass('error bubbled up'); 29 | t.end(); 30 | }); 31 | } 32 | 33 | MuSystemScheduler.requestAnimationFrame(() => { 34 | throw new Error('foo'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/schema/_number.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuSchema } from './schema'; 3 | 4 | function tuple (...args:T) : T { 5 | return args; 6 | } 7 | 8 | export const ranges = { 9 | float32: tuple(-3.4028234663852886e+38, 3.4028234663852886e+38), 10 | float64: tuple(-1.7976931348623157e+308, 1.7976931348623157e+308), 11 | 12 | int8: tuple(-0x80, 0x7f), 13 | int16: tuple(-0x8000, 0x7fff), 14 | int32: tuple(-0x80000000, 0x7fffffff), 15 | 16 | uint8: tuple(0, 0xff), 17 | uint16: tuple(0, 0xffff), 18 | uint32: tuple(0, 0xffffffff), 19 | 20 | varint: tuple(0, 0xffffffff), 21 | rvarint: tuple(0, 0xffffffff), 22 | }; 23 | 24 | export type MuNumericType = 25 | 'float32' | 26 | 'float64' | 27 | 'int8' | 28 | 'int16' | 29 | 'int32' | 30 | 'uint8' | 31 | 'uint16' | 32 | 'uint32' | 33 | 'varint' | 34 | 'rvarint'; 35 | 36 | export abstract class MuNumber implements MuSchema { 37 | public readonly muType:T; 38 | public readonly identity:number; 39 | public readonly json:object; 40 | 41 | constructor (identity_:number|undefined, type:T) { 42 | const identity = identity_ === identity_ ? identity_ || 0 : NaN; 43 | const range = ranges[type]; 44 | 45 | if (identity !== Infinity && identity !== -Infinity && identity === identity) { 46 | if (identity < range[0] || identity > range[1]) { 47 | throw new RangeError(`${identity} is out of range of ${type}`); 48 | } 49 | } else if (type !== 'float32' && type !== 'float64') { 50 | throw new RangeError(`${identity} is out of range of ${type}`); 51 | } 52 | 53 | this.identity = identity; 54 | this.muType = type; 55 | this.json = { 56 | type, 57 | identity, 58 | }; 59 | } 60 | 61 | public alloc () : number { return this.identity; } 62 | 63 | public free (num:number) : void { } 64 | 65 | public equal (a:number, b:number) : boolean { return a === b; } 66 | 67 | public clone (num:number) : number { return num; } 68 | 69 | public assign (dst:number, src:number) : number { return src; } 70 | 71 | public toJSON (num:number) : number { return num; } 72 | 73 | public fromJSON (x:number) : number { 74 | if (typeof x === 'number') { 75 | return x; 76 | } 77 | return this.identity; 78 | } 79 | 80 | public abstract diff (base:number, target:number, out:MuWriteStream) : boolean; 81 | 82 | public abstract patch (base:number, inp:MuReadStream) : number; 83 | } 84 | -------------------------------------------------------------------------------- /src/schema/_string.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuSchema } from './schema'; 3 | 4 | export type MuStringType = 5 | 'ascii' | 6 | 'fixed-ascii' | 7 | 'utf8'; 8 | 9 | export abstract class MuString implements MuSchema { 10 | public readonly muType:T; 11 | public readonly identity:string; 12 | public readonly json:object; 13 | 14 | constructor (identity:string, type:T) { 15 | this.identity = identity; 16 | this.muType = type; 17 | this.json = { 18 | type, 19 | identity, 20 | }; 21 | } 22 | 23 | public alloc () : string { return this.identity; } 24 | 25 | public free (str:string) : void { } 26 | 27 | public equal (a:string, b:string) : boolean { return a === b; } 28 | 29 | public clone (str:string) : string { return str; } 30 | 31 | public assign (dst:string, src:string) : string { return src; } 32 | 33 | public toJSON (str:string) : string { return str; } 34 | 35 | public fromJSON (x:string) : string { 36 | if (typeof x === 'string') { 37 | return x; 38 | } 39 | return this.identity; 40 | } 41 | 42 | public abstract diff (base:string, target:string, out:MuWriteStream) : boolean; 43 | 44 | public abstract patch (base:string, inp:MuReadStream) : string; 45 | } 46 | -------------------------------------------------------------------------------- /src/schema/ascii.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuString } from './_string'; 3 | 4 | export class MuASCII extends MuString<'ascii'> { 5 | constructor (identity?:string) { 6 | super(identity || '', 'ascii'); 7 | } 8 | 9 | public diff (base:string, target:string, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(5 + target.length); 12 | out.writeVarint(target.length); 13 | out.writeASCII(target); 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | public patch (base:string, inp:MuReadStream) : string { 20 | return inp.readASCII(inp.readVarint()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/schema/bench/_do.ts: -------------------------------------------------------------------------------- 1 | import { performance, PerformanceObserver } from 'perf_hooks'; 2 | import { MuWriteStream, MuReadStream } from '../../stream'; 3 | import { MuSchema } from '../schema'; 4 | 5 | export function deltaByteLength (schema:MuSchema, a:T, b:T) { 6 | const ws = new MuWriteStream(1); 7 | schema.diff(a, b, ws); 8 | console.log(`${JSON.stringify(a)} -> ${JSON.stringify(b)}: ${ws.bytes().length}`); 9 | } 10 | 11 | export function diffPatchDuration (schema:MuSchema, a:T, b:T, rounds:number, id='', sampleSize=9) { 12 | const diffSample:number[] = []; 13 | const diffObserver = new PerformanceObserver((list) => { 14 | const entry = list.getEntriesByName(`diff`)[0]; 15 | if (entry) { 16 | performance.clearMarks(); 17 | diffSample.push(entry.duration); 18 | if (diffSample.length === sampleSize) { 19 | diffObserver.disconnect(); 20 | diffSample.sort((x, y) => x - y); 21 | const median = diffSample[sampleSize >>> 1]; 22 | console.log((id && `${id}: `) + `diff ${rounds} rounds: ${median}`); 23 | } 24 | } 25 | }); 26 | diffObserver.observe({ entryTypes: ['measure'] }); 27 | 28 | const wss:MuWriteStream[] = new Array(sampleSize * rounds); 29 | for (let i = 0; i < wss.length; ++i) { 30 | wss[i] = new MuWriteStream(1); 31 | } 32 | 33 | for (let i = 0; i < sampleSize; ++i) { 34 | performance.mark('A'); 35 | for (let j = 0; j < rounds; ++j) { 36 | const ws = wss[i * rounds + j]; 37 | schema.diff(a, b, ws); 38 | } 39 | performance.mark('B'); 40 | performance.measure(`diff`, 'A', 'B'); 41 | } 42 | 43 | const patchSample:number[] = []; 44 | const patchObserver = new PerformanceObserver((list) => { 45 | const entry = list.getEntriesByName(`patch`)[0]; 46 | if (entry) { 47 | performance.clearMarks(); 48 | patchSample.push(entry.duration); 49 | if (patchSample.length === sampleSize) { 50 | patchObserver.disconnect(); 51 | patchSample.sort((x, y) => x - y); 52 | const median = patchSample[sampleSize >>> 1]; 53 | console.log((id && `${id}: `) + `patch ${rounds} rounds: ${median}`); 54 | } 55 | } 56 | }); 57 | patchObserver.observe({ entryTypes: ['measure'] }); 58 | 59 | const rss:MuReadStream[] = wss.map((ws) => new MuReadStream(ws.bytes())); 60 | 61 | for (let i = 0; i < sampleSize; ++i) { 62 | performance.mark('C'); 63 | for (let j = 0; j < rounds; ++j) { 64 | const rs = rss[i * rounds + j]; 65 | (rs.offset < rs.length) && schema.patch(a, rs); 66 | } 67 | performance.mark('D'); 68 | performance.measure(`patch`, 'C', 'D'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/schema/bench/array.ts: -------------------------------------------------------------------------------- 1 | import { MuArray, MuASCII } from '../'; 2 | import { deltaByteLength, diffPatchDuration } from './_do'; 3 | 4 | const array = new MuArray(new MuASCII(), Infinity); 5 | 6 | deltaByteLength(array, [], ['a']); 7 | deltaByteLength(array, [], ['a', 'b']); 8 | deltaByteLength(array, [], ['a', 'b', 'c']); 9 | deltaByteLength(array, ['a'], ['a', 'b']); 10 | deltaByteLength(array, ['b'], ['a', 'b']); 11 | 12 | deltaByteLength(array, ['a'], []); 13 | deltaByteLength(array, ['a', 'b'], []); 14 | deltaByteLength(array, ['a', 'b'], ['a']); 15 | deltaByteLength(array, ['a', 'b', 'c'], ['a']); 16 | 17 | deltaByteLength(array, ['a'], ['b']); 18 | deltaByteLength(array, ['a', 'b'], ['b', 'c']); 19 | 20 | const a0 = []; 21 | const a1 = ['a', 'b', 'c', 'd', 'e']; 22 | 23 | diffPatchDuration(array, a1, a1, 1e3, 'b=t'); 24 | 25 | diffPatchDuration(array, a0, a1, 1e3, 'b!=t'); 26 | diffPatchDuration(array, a0, a1, 1e4, 'b!=t'); 27 | diffPatchDuration(array, a0, a1, 1e5, 'b!=t'); 28 | -------------------------------------------------------------------------------- /src/schema/bench/date.ts: -------------------------------------------------------------------------------- 1 | import { MuDate } from '../date'; 2 | import { deltaByteLength, diffPatchDuration } from './_do'; 3 | 4 | const date = new MuDate(new Date(0)); 5 | deltaByteLength(date, new Date(0), new Date()); 6 | diffPatchDuration(date, new Date(0), new Date(), 1e3); 7 | -------------------------------------------------------------------------------- /src/schema/bench/dictionary.ts: -------------------------------------------------------------------------------- 1 | import { MuDictionary, MuUint8 } from '../'; 2 | import { deltaByteLength, diffPatchDuration } from './_do'; 3 | 4 | const dict = new MuDictionary(new MuUint8(), Infinity); 5 | 6 | deltaByteLength(dict, {}, {a: 0}); 7 | deltaByteLength(dict, {}, {a: 0, b: 1}); 8 | deltaByteLength(dict, {}, {a: 0, b: 1, c: 2}); 9 | deltaByteLength(dict, {}, {pool: 0}); 10 | deltaByteLength(dict, {}, {pool: 0, preface: 1}); 11 | deltaByteLength(dict, {}, {pool: 0, preface: 1, prefix: 2}); 12 | deltaByteLength(dict, {}, {pool: 0, preface: 1, prefix: 2, prefixed: 3}); 13 | 14 | const d0 = {}; 15 | const d1 = {a: 0, b: 0, c: 0}; 16 | const d2 = {pool: 0, preface: 1, prefix: 2, prefixed: 3}; 17 | 18 | diffPatchDuration(dict, d1, d1, 1e3, 'b=t'); 19 | 20 | diffPatchDuration(dict, d0, d1, 1e3, 'no common prefix'); 21 | diffPatchDuration(dict, d0, d1, 1e4, 'no common prefix'); 22 | diffPatchDuration(dict, d0, d1, 1e5, 'no common prefix'); 23 | 24 | diffPatchDuration(dict, d0, d2, 1e3, 'common prefix'); 25 | diffPatchDuration(dict, d0, d2, 1e4, 'common prefix'); 26 | diffPatchDuration(dict, d0, d2, 1e5, 'common prefix'); 27 | -------------------------------------------------------------------------------- /src/schema/bench/sorted-array.ts: -------------------------------------------------------------------------------- 1 | import { MuSortedArray, MuUint8 } from '../'; 2 | import { deltaByteLength, diffPatchDuration } from './_do'; 3 | 4 | const uint8SortedArray = new MuSortedArray(new MuUint8(), Infinity); 5 | 6 | deltaByteLength(uint8SortedArray, [], []); 7 | deltaByteLength(uint8SortedArray, [0], [0]); 8 | 9 | deltaByteLength(uint8SortedArray, [], [0]); 10 | deltaByteLength(uint8SortedArray, [], [0, 1]); 11 | deltaByteLength(uint8SortedArray, [], [0, 1, 2]); 12 | deltaByteLength(uint8SortedArray, [0], [0, 1]); 13 | deltaByteLength(uint8SortedArray, [1], [0, 1]); 14 | 15 | deltaByteLength(uint8SortedArray, [0], []); 16 | deltaByteLength(uint8SortedArray, [0, 1], []); 17 | deltaByteLength(uint8SortedArray, [0, 1], [0]); 18 | deltaByteLength(uint8SortedArray, [0, 1, 2], [0]); 19 | 20 | deltaByteLength(uint8SortedArray, [0], [1]); 21 | deltaByteLength(uint8SortedArray, [0, 1], [1, 2]); 22 | 23 | const a1 = [0, 1, 2, 4, 5]; 24 | const a2 = [1, 3, 5, 7, 9]; 25 | 26 | diffPatchDuration(uint8SortedArray, a1, a1, 1e3, 'b=t'); 27 | 28 | diffPatchDuration(uint8SortedArray, a1, a2, 1e3, 'b!=t'); 29 | diffPatchDuration(uint8SortedArray, a1, a2, 1e4, 'b!=t'); 30 | diffPatchDuration(uint8SortedArray, a1, a2, 1e5, 'b!=t'); 31 | -------------------------------------------------------------------------------- /src/schema/bench/struct.ts: -------------------------------------------------------------------------------- 1 | import { MuStruct, MuUint8, MuVarint, MuRelativeVarint, MuVector, MuFloat32 } from '../'; 2 | import { deltaByteLength, diffPatchDuration } from './_do'; 3 | 4 | { 5 | const struct = new MuStruct({ 6 | a: new MuUint8(), 7 | b: new MuUint8(), 8 | }); 9 | 10 | deltaByteLength(struct, {a: 0, b: 0}, {a:1, b: 0}); 11 | deltaByteLength(struct, {a: 0, b: 0}, {a:1, b: 2}); 12 | 13 | const s0 = {a: 0, b: 0}; 14 | const s1 = {a: 1, b: 2}; 15 | 16 | diffPatchDuration(struct, s1, s1, 1e3, 'uint8 - b=t'); 17 | diffPatchDuration(struct, s0, s1, 1e3, 'uint8 - b!=t'); 18 | diffPatchDuration(struct, s0, s1, 1e4, 'uint8 - b!=t'); 19 | diffPatchDuration(struct, s0, s1, 1e5, 'uint8 - b!=t'); 20 | } 21 | 22 | { 23 | const struct = new MuStruct({ 24 | v: new MuVarint(), 25 | rv: new MuRelativeVarint(), 26 | }); 27 | 28 | deltaByteLength(struct, {v: 0, rv: 0}, {v: 0x7f, rv: 0}); 29 | deltaByteLength(struct, {v: 0, rv: 0}, {v: 0x80, rv: 0}); 30 | deltaByteLength(struct, {v: 0, rv: 0}, {v: 0x80, rv: -0x2a}); 31 | deltaByteLength(struct, {v: 0, rv: 0}, {v: 0x80, rv: -0x2b}); 32 | 33 | const s0 = {v: 0, rv: 0}; 34 | const s1 = {v: 0x7f, rv: -0x2a}; 35 | const s2 = {v: 0x80, rv: -0x2b}; 36 | 37 | diffPatchDuration(struct, s1, s1, 1e3, 'varint - b=t'); 38 | diffPatchDuration(struct, s0, s1, 1e3, 'varint - one byte'); 39 | diffPatchDuration(struct, s0, s1, 1e4, 'varint - one byte'); 40 | diffPatchDuration(struct, s0, s1, 1e5, 'varint - one byte'); 41 | diffPatchDuration(struct, s0, s2, 1e3, 'varint - two bytes'); 42 | diffPatchDuration(struct, s0, s2, 1e4, 'varint - two bytes'); 43 | diffPatchDuration(struct, s0, s2, 1e5, 'varint - two bytes'); 44 | } 45 | 46 | { 47 | const struct = new MuStruct({ 48 | vec2: new MuVector(new MuFloat32(), 2), 49 | vec3: new MuVector(new MuFloat32(), 3), 50 | }); 51 | 52 | const s0 = { 53 | vec2: Float32Array.from([0, 0]), 54 | vec3: Float32Array.from([0, 0, 0]), 55 | }; 56 | const s1 = { 57 | vec2: Float32Array.from([0.5, 1.5]), 58 | vec3: Float32Array.from([0.5, 1.5, 2.5]), 59 | }; 60 | 61 | diffPatchDuration(struct, s0, s0, 1e3, 'vector - b=t'); 62 | diffPatchDuration(struct, s0, s1, 1e3, 'vector - b!=t'); 63 | diffPatchDuration(struct, s0, s1, 1e4, 'vector - b!=t'); 64 | diffPatchDuration(struct, s0, s1, 1e5, 'vector - b!=t'); 65 | } 66 | -------------------------------------------------------------------------------- /src/schema/bench/vector.ts: -------------------------------------------------------------------------------- 1 | import { MuVector, MuFloat32 } from '../'; 2 | import { deltaByteLength, diffPatchDuration } from './_do'; 3 | 4 | const vec3 = new MuVector(new MuFloat32(), 3); 5 | 6 | const from = (a) => Float32Array.from(a); 7 | 8 | deltaByteLength(vec3, from([0, 0, 0]), from([0.5, 0.5, 0.5])); 9 | deltaByteLength(vec3, from([0, 0, 0]), from([1.5, 1.5, 1.5])); 10 | 11 | const v1 = from([0, 0, 0]); 12 | const v2 = from([0.5, 1, 1.5]); 13 | 14 | diffPatchDuration(vec3, v2, v2, 1e3, 'b=t'); 15 | 16 | diffPatchDuration(vec3, v1, v2, 1e3, 'b!=t'); 17 | diffPatchDuration(vec3, v1, v2, 1e4, 'b!=t'); 18 | diffPatchDuration(vec3, v1, v2, 1e5, 'b!=t'); 19 | -------------------------------------------------------------------------------- /src/schema/boolean.ts: -------------------------------------------------------------------------------- 1 | import { MuReadStream, MuWriteStream } from '../stream'; 2 | import { MuSchema } from './schema'; 3 | 4 | export class MuBoolean implements MuSchema { 5 | public readonly muType = 'boolean'; 6 | public readonly identity:boolean; 7 | public readonly json:object; 8 | 9 | constructor (identity?:boolean) { 10 | this.identity = !!identity; 11 | this.json = { 12 | type: 'boolean', 13 | identity: this.identity, 14 | }; 15 | } 16 | 17 | public alloc () : boolean { return this.identity; } 18 | 19 | public free (bool:boolean) : void { } 20 | 21 | public equal (a:boolean, b:boolean) : boolean { return a === b; } 22 | 23 | public clone (bool:boolean) : boolean { return bool; } 24 | 25 | public assign (dst:boolean, src:boolean) : boolean { return src; } 26 | 27 | public diff (base:boolean, target:boolean, out:MuWriteStream) : boolean { 28 | if (base !== target) { 29 | out.grow(1); 30 | out.writeUint8(target ? 1 : 0); 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | public patch (base:boolean, inp:MuReadStream) : boolean { 37 | const result = inp.readUint8(); 38 | if (result > 1) { 39 | throw new Error(`invalid value for boolean`); 40 | } 41 | return !!result; 42 | } 43 | 44 | public toJSON (bool:boolean) : boolean { return bool; } 45 | 46 | public fromJSON (x:boolean) : boolean { 47 | if (typeof x === 'boolean') { 48 | return x; 49 | } 50 | return this.identity; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/schema/bytes.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from '../schema'; 2 | import { MuWriteStream, MuReadStream } from '../stream'; 3 | 4 | export class MuBytes implements MuSchema { 5 | public readonly muType = 'bytes'; 6 | public readonly identity:Uint8Array; 7 | public readonly json:object; 8 | 9 | public pool:{ [dimension:string]:Uint8Array[] } = {}; 10 | 11 | constructor (identity?:Uint8Array) { 12 | if (identity) { 13 | this.identity = identity.slice(); 14 | } else { 15 | this.identity = new Uint8Array(1); 16 | } 17 | 18 | this.json = { 19 | type: 'bytes', 20 | identity: `[${(Array.prototype.slice.call(this.identity).join())}]`, 21 | }; 22 | } 23 | 24 | private _allocBytes (length:number) : Uint8Array { 25 | return (this.pool[length] && this.pool[length].pop()) || new Uint8Array(length); 26 | } 27 | 28 | public alloc () : Uint8Array { 29 | return this._allocBytes(this.identity.length); 30 | } 31 | 32 | public free (bytes:Uint8Array) { 33 | const length = bytes.length; 34 | if (!this.pool[length]) { 35 | this.pool[length] = []; 36 | } 37 | this.pool[length].push(bytes); 38 | } 39 | 40 | public equal (a:Uint8Array, b:Uint8Array) { 41 | if (a.length !== b.length) { 42 | return false; 43 | } 44 | for (let i = a.length - 1; i >= 0; --i) { 45 | if (a[i] !== b[i]) { 46 | return false; 47 | } 48 | } 49 | return true; 50 | } 51 | 52 | public clone (bytes:Uint8Array) : Uint8Array { 53 | const copy = this._allocBytes(bytes.length); 54 | copy.set(bytes); 55 | return copy; 56 | } 57 | 58 | public assign (dst:Uint8Array, src:Uint8Array) : Uint8Array { 59 | if (dst.length !== src.length) { 60 | throw new Error('dst and src are of different lengths'); 61 | } 62 | dst.set(src); 63 | return dst; 64 | } 65 | 66 | public diff (base:Uint8Array, target:Uint8Array, out:MuWriteStream) : boolean { 67 | const length = target.length; 68 | out.grow(5 + length); 69 | 70 | out.writeVarint(length); 71 | out.buffer.uint8.set(target, out.offset); 72 | out.offset += length; 73 | 74 | return true; 75 | } 76 | 77 | public patch (base:Uint8Array, inp:MuReadStream) : Uint8Array { 78 | const length = inp.readVarint(); 79 | const target = this._allocBytes(length); 80 | 81 | const bytes = inp.buffer.uint8.subarray(inp.offset, inp.offset += length); 82 | target.set(bytes); 83 | return target; 84 | } 85 | 86 | public toJSON (bytes:Uint8Array) : number[] { 87 | const arr = new Array(bytes.length); 88 | for (let i = 0; i < arr.length; ++i) { 89 | arr[i] = bytes[i]; 90 | } 91 | return arr; 92 | } 93 | 94 | public fromJSON (x:number[]) : Uint8Array { 95 | if (Array.isArray(x)) { 96 | const bytes = this._allocBytes(x.length); 97 | bytes.set(x); 98 | return bytes; 99 | } 100 | return this.clone(this.identity); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/schema/date.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuSchema } from './schema'; 3 | 4 | export class MuDate implements MuSchema { 5 | public readonly muType = 'date'; 6 | public readonly identity:Date; 7 | public readonly json; 8 | 9 | constructor (identity?:Date) { 10 | this.identity = new Date(0); 11 | if (identity) { 12 | this.identity.setTime(identity.getTime()); 13 | } 14 | this.json = { 15 | type: 'date', 16 | identity: this.identity.toISOString(), 17 | }; 18 | } 19 | 20 | public alloc () : Date { 21 | return new Date(); 22 | } 23 | 24 | public free (date:Date) : void { 25 | } 26 | 27 | public equal (a:Date, b:Date) : boolean { 28 | return a.getTime() === b.getTime(); 29 | } 30 | 31 | public clone (date_:Date) : Date { 32 | const date = this.alloc(); 33 | date.setTime(date_.getTime()); 34 | return date; 35 | } 36 | 37 | public assign (dst:Date, src:Date) : Date { 38 | dst.setTime(src.getTime()); 39 | return dst; 40 | } 41 | 42 | public diff (base:Date, target:Date, out:MuWriteStream) : boolean { 43 | const bt = base.getTime(); 44 | const tt = target.getTime(); 45 | if (bt !== tt) { 46 | out.grow(10); 47 | out.writeVarint(tt % 0x10000000); 48 | out.writeVarint(tt / 0x10000000 | 0); 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | public patch (base:Date, inp:MuReadStream) : Date { 55 | const date = this.alloc(); 56 | const lo = inp.readVarint(); 57 | const hi = inp.readVarint(); 58 | date.setTime(lo + 0x10000000 * hi); 59 | return date; 60 | } 61 | 62 | public toJSON (date:Date) : string { 63 | return date.toISOString(); 64 | } 65 | 66 | public fromJSON (x:string) : Date { 67 | if (typeof x === 'string') { 68 | return new Date(x); 69 | } 70 | return this.clone(this.identity); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/schema/fixed-ascii.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuString } from './_string'; 3 | 4 | export class MuFixedASCII extends MuString<'fixed-ascii'> { 5 | public readonly length:number; 6 | 7 | constructor (lengthOrIdentity:number|string) { 8 | const identity = typeof lengthOrIdentity === 'number' ? 9 | new Array(lengthOrIdentity + 1).join(' ') : 10 | lengthOrIdentity; 11 | 12 | super(identity, 'fixed-ascii'); 13 | this.length = identity.length; 14 | } 15 | 16 | public diff (base:string, target:string, out:MuWriteStream) : boolean { 17 | if (base !== target) { 18 | out.grow(this.length); 19 | out.writeASCII(target); 20 | return true; 21 | } 22 | return false; 23 | } 24 | 25 | public patch (base:string, inp:MuReadStream) : string { 26 | return inp.readASCII(this.length); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/float32.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuFloat32 extends MuNumber<'float32'> { 5 | constructor(identity?:number) { 6 | super(identity, 'float32'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(4); 12 | out.writeFloat32(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readFloat32(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/float64.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuFloat64 extends MuNumber<'float64'> { 5 | constructor(identity?:number) { 6 | super(identity, 'float64'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(8); 12 | out.writeFloat64(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readFloat64(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | // schema type interface 2 | import { MuSchema } from './schema'; 3 | 4 | // bottom type 5 | import { MuVoid } from './void'; 6 | 7 | // boolean type 8 | import { MuBoolean } from './boolean'; 9 | 10 | // string types 11 | import { MuASCII } from './ascii'; 12 | import { MuFixedASCII } from './fixed-ascii'; 13 | import { MuUTF8 } from './utf8'; 14 | 15 | // number types 16 | import { MuFloat32 } from './float32'; 17 | import { MuFloat64 } from './float64'; 18 | import { MuInt8 } from './int8'; 19 | import { MuInt16 } from './int16'; 20 | import { MuInt32 } from './int32'; 21 | import { MuUint8 } from './uint8'; 22 | import { MuUint16 } from './uint16'; 23 | import { MuUint32 } from './uint32'; 24 | import { MuVarint } from './varint'; 25 | import { MuRelativeVarint } from './rvarint'; 26 | import { MuQuantizedFloat } from './quantized-float'; 27 | 28 | // functors 29 | import { MuArray } from './array'; 30 | import { MuOption } from './option'; 31 | import { MuNullable } from './nullable'; 32 | import { MuSortedArray } from './sorted-array'; 33 | import { MuStruct } from './struct'; 34 | import { MuUnion } from './union'; 35 | 36 | // data structures 37 | import { MuBytes } from './bytes'; 38 | import { MuDictionary } from './dictionary'; 39 | import { MuVector } from './vector'; 40 | 41 | // misc. types 42 | import { MuDate } from './date'; 43 | import { MuJSON } from './json'; 44 | 45 | export { 46 | MuSchema, 47 | 48 | MuVoid, 49 | MuBoolean, 50 | MuASCII, 51 | MuFixedASCII, 52 | MuUTF8, 53 | MuFloat32, 54 | MuFloat64, 55 | MuInt8, 56 | MuInt16, 57 | MuInt32, 58 | MuUint8, 59 | MuUint16, 60 | MuUint32, 61 | MuVarint, 62 | MuRelativeVarint, 63 | MuQuantizedFloat, 64 | 65 | MuArray, 66 | MuOption, 67 | MuNullable, 68 | MuSortedArray, 69 | MuStruct, 70 | MuUnion, 71 | 72 | MuBytes, 73 | MuVector, 74 | MuDictionary, 75 | 76 | MuDate, 77 | MuJSON, 78 | }; 79 | -------------------------------------------------------------------------------- /src/schema/int16.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuInt16 extends MuNumber<'int16'> { 5 | constructor(identity?:number) { 6 | super(identity, 'int16'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(2); 12 | out.writeInt16(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readInt16(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/int32.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuInt32 extends MuNumber<'int32'> { 5 | constructor(identity?:number) { 6 | super(identity, 'int32'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(4); 12 | out.writeInt32(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readInt32(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/int8.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuInt8 extends MuNumber<'int8'> { 5 | constructor(identity?:number) { 6 | super(identity, 'int8'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(1); 12 | out.writeInt8(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readInt8(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/is-primitive.ts: -------------------------------------------------------------------------------- 1 | const muPrimitiveTypes = [ 2 | 'ascii', 3 | 'boolean', 4 | 'fixed-ascii', 5 | 'float32', 6 | 'float64', 7 | 'int8', 8 | 'int16', 9 | 'int32', 10 | 'uint8', 11 | 'uint16', 12 | 'uint32', 13 | 'utf8', 14 | 'void', 15 | ]; 16 | 17 | export function isMuPrimitiveType (muType:string) : boolean { 18 | return muPrimitiveTypes.indexOf(muType) > -1; 19 | } 20 | -------------------------------------------------------------------------------- /src/schema/quantized-float.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from './schema'; 2 | import { MuWriteStream, MuReadStream } from '../stream'; 3 | 4 | const SCHROEPPEL2 = 0xAAAAAAAA; 5 | 6 | function readSchroeppel (stream:MuReadStream) { 7 | const x = stream.readVarint(); 8 | return ((SCHROEPPEL2 ^ x) - SCHROEPPEL2) >> 0; 9 | } 10 | 11 | export class MuQuantizedFloat implements MuSchema { 12 | public invPrecision = 1; 13 | public identity = 0; 14 | public json:{ 15 | type:'quantized-float'; 16 | precision:number; 17 | identity:number; 18 | }; 19 | public muData:{ 20 | type:'quantized-float'; 21 | precision:number; 22 | identity:number; 23 | } = { 24 | type: 'quantized-float', 25 | precision: 0, 26 | identity: 0, 27 | }; 28 | public readonly muType = 'quantized-float'; 29 | 30 | constructor ( 31 | public precision:number, 32 | identity?:number) { 33 | this.invPrecision = 1 / this.precision; 34 | if (identity) { 35 | this.identity = this.precision * ((this.invPrecision * identity) >> 0); 36 | } 37 | this.json = this.muData = { 38 | type: 'quantized-float', 39 | precision: this.precision, 40 | identity: this.identity, 41 | }; 42 | } 43 | 44 | public assign(x:number, y:number) { 45 | return ((this.invPrecision * y) >> 0) * this.precision; 46 | } 47 | 48 | public clone (y:number) { 49 | return ((this.invPrecision * y) >> 0) * this.precision; 50 | } 51 | 52 | public alloc () { 53 | return this.identity; 54 | } 55 | 56 | public free () {} 57 | 58 | public toJSON (x:number) { 59 | return this.precision * ((this.invPrecision * x) >> 0); 60 | } 61 | 62 | public fromJSON (x:any) { 63 | if (typeof x === 'number') { 64 | return this.clone(x); 65 | } 66 | return this.identity; 67 | } 68 | 69 | public equal (x:number, y:number) { 70 | const sf = this.invPrecision; 71 | return ((sf * x) >> 0) === ((sf * y) >> 0); 72 | } 73 | 74 | public diff (base:number, target:number, stream:MuWriteStream) { 75 | const sf = this.invPrecision; 76 | const b = (sf * base) >> 0; 77 | const t = (sf * target) >> 0; 78 | if (b === t) { 79 | return false; 80 | } 81 | stream.grow(5); 82 | stream.writeVarint((SCHROEPPEL2 + (t - b) ^ SCHROEPPEL2) >>> 0); 83 | return true; 84 | } 85 | 86 | public patch (base:number, stream:MuReadStream) { 87 | const b = (this.invPrecision * base) >> 0; 88 | const d = readSchroeppel(stream); 89 | return (b + d) * this.precision; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/schema/rvarint.ts: -------------------------------------------------------------------------------- 1 | import { MuNumber } from './_number'; 2 | import { MuWriteStream, MuReadStream } from '../stream'; 3 | 4 | const SCHROEPPEL2 = 0xAAAAAAAA; 5 | 6 | export class MuRelativeVarint extends MuNumber<'rvarint'> { 7 | constructor (identity?:number) { 8 | super(identity, 'rvarint'); 9 | } 10 | 11 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 12 | if (base !== target) { 13 | out.grow(5); 14 | out.writeVarint((SCHROEPPEL2 + (target - base)) ^ SCHROEPPEL2); 15 | return true; 16 | } 17 | return false; 18 | } 19 | 20 | public patch (base:number, inp:MuReadStream) : number { 21 | const delta = (SCHROEPPEL2 ^ inp.readVarint()) - SCHROEPPEL2 >> 0; 22 | return base + delta; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import { MuReadStream, MuWriteStream } from '../stream'; 2 | 3 | export interface MuSchema { 4 | /** Base value */ 5 | readonly identity:Value; 6 | 7 | /** Run time type info */ 8 | readonly muType:string; 9 | 10 | /** Additional schema-specific type info */ 11 | readonly muData?:any; 12 | 13 | /** JSON description of schema, used to compare schemas */ 14 | readonly json:object; 15 | 16 | /** Allocates a new value */ 17 | alloc () : Value; 18 | 19 | /** Returns `value` to memory pool */ 20 | free (value:Value) : void; 21 | 22 | /** Checks equality of `a` and `b` */ 23 | equal (a:Value, b:Value) : boolean; 24 | 25 | /** Makes a copy of `value` */ 26 | clone (value:Value) : Value; 27 | 28 | /** Assigns `dst` the content of `src` */ 29 | assign (dst:Value, src:Value) : Value; 30 | 31 | /** Computes a binary patch from `base` to `target` */ 32 | diff (base:Value, target:Value, out:MuWriteStream) : boolean; 33 | 34 | /** Applies a binary patch to `base` */ 35 | patch (base:Value, inp:MuReadStream) : Value; 36 | 37 | /** Creates a JSON serializable object from `value` */ 38 | toJSON (value:Value) : any; 39 | 40 | /** Creates a value conforming to schema from `json` */ 41 | fromJSON (json:any) : Value; 42 | } 43 | -------------------------------------------------------------------------------- /src/schema/test/capacity.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { MuWriteStream, MuReadStream } from '../../stream'; 3 | import { 4 | MuFloat32, 5 | MuArray, 6 | MuSortedArray, 7 | MuDictionary, 8 | } from '../index'; 9 | 10 | test('guarding array.patch()', (t) => { 11 | const infiniteArray = new MuArray(new MuFloat32(), Infinity); 12 | const out = new MuWriteStream(1); 13 | infiniteArray.diff([], [1], out); 14 | infiniteArray.diff([], [1, 2, 3], out); 15 | infiniteArray.diff([], [1, 2, 3, 4], out); 16 | 17 | const finiteArray = new MuArray(new MuFloat32(), 3); 18 | const inp = new MuReadStream(out.buffer.uint8); 19 | t.doesNotThrow(() => finiteArray.patch([], inp)); 20 | t.doesNotThrow(() => finiteArray.patch([], inp)); 21 | t.throws(() => finiteArray.patch([], inp), RangeError); 22 | t.end(); 23 | }); 24 | 25 | test('guarding sortedArray.patch()', (t) => { 26 | const infiniteSortedArray = new MuSortedArray(new MuFloat32(), Infinity); 27 | const out = new MuWriteStream(1); 28 | infiniteSortedArray.diff([], [1], out); 29 | infiniteSortedArray.diff([], [1, 2, 3], out); 30 | infiniteSortedArray.diff([], [1, 2, 3, 4], out); 31 | 32 | const finiteSortedArray = new MuSortedArray(new MuFloat32(), 3); 33 | const inp = new MuReadStream(out.buffer.uint8); 34 | t.doesNotThrow(() => finiteSortedArray.patch([], inp)); 35 | t.doesNotThrow(() => finiteSortedArray.patch([], inp)); 36 | t.throws(() => finiteSortedArray.patch([], inp), RangeError); 37 | t.end(); 38 | }); 39 | 40 | test('guarding dictionary.patch()', (t) => { 41 | const infiniteDictionary = new MuDictionary(new MuFloat32(), Infinity); 42 | const out = new MuWriteStream(1); 43 | infiniteDictionary.diff({}, {a: 0, b: 0, c: 0, d: 0}, out); 44 | 45 | const finiteDictionary = new MuDictionary(new MuFloat32(), 3); 46 | const inp = new MuReadStream(out.buffer.uint8); 47 | t.throws(() => finiteDictionary.patch({}, inp), Error); 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /src/schema/test/free.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { 3 | MuFloat32, 4 | MuArray, 5 | MuOption, 6 | MuSortedArray, 7 | MuStruct, 8 | MuUnion, 9 | MuDictionary, 10 | MuVector, 11 | MuBytes, 12 | } from '../index'; 13 | import { MuSchemaTrace } from '../trace'; 14 | 15 | test('schema.free()', (t) => { 16 | const arrayTrace = new MuSchemaTrace( 17 | new MuArray(new MuFloat32(), Infinity), 18 | ); 19 | const array = new MuArray(arrayTrace, Infinity); 20 | const a = array.alloc(); 21 | a.push(arrayTrace.alloc()); 22 | array.free(a); 23 | t.equal(arrayTrace.freeCount, 1, 'array schema should call free() on subtype'); 24 | 25 | const sortedArrayTrace = new MuSchemaTrace( 26 | new MuSortedArray(new MuFloat32(), Infinity), 27 | ); 28 | const sortedArray = new MuSortedArray(sortedArrayTrace, Infinity); 29 | const sa = sortedArray.alloc(); 30 | sa.push(sortedArrayTrace.alloc()); 31 | sortedArray.free(sa); 32 | t.equal(sortedArrayTrace.freeCount, 1, 'sorted array schema should call free() on subtype'); 33 | 34 | const structTrace = new MuSchemaTrace( 35 | new MuStruct({ f: new MuFloat32() }), 36 | ); 37 | const struct = new MuStruct({ s: structTrace }); 38 | const s = struct.alloc(); 39 | struct.free(s); 40 | t.equal(structTrace.freeCount, 1, 'struct schema should call free() on subtype'); 41 | 42 | const vectorTrace = new MuSchemaTrace( 43 | new MuVector(new MuFloat32(), 3), 44 | ); 45 | const union = new MuUnion({ 46 | v: vectorTrace, 47 | }, 'v'); 48 | const u = union.alloc(); 49 | union.free(u); 50 | t.equal(vectorTrace.freeCount, 1, 'union schema should call free() on subtype'); 51 | 52 | const vectorTrace2 = new MuSchemaTrace( 53 | new MuVector(new MuFloat32(), 3), 54 | ); 55 | const option = new MuOption(vectorTrace2); 56 | const o = option.alloc(); 57 | option.free(o); 58 | t.equal(vectorTrace2.freeCount, 1, 'option schema should call free() on subtype'); 59 | t.doesNotThrow( 60 | () => { option.free(undefined); }, 61 | 'option schema can call free on undefined', 62 | ); 63 | 64 | const dictionaryTrace = new MuSchemaTrace( 65 | new MuDictionary(new MuFloat32(), Infinity), 66 | ); 67 | const dictionary = new MuDictionary(dictionaryTrace, Infinity); 68 | const d = dictionary.alloc(); 69 | d.d = dictionaryTrace.alloc(); 70 | dictionary.free(d); 71 | t.equal(dictionaryTrace.freeCount, 1, 'dictionary schema should call free() on subtype'); 72 | t.end(); 73 | }); 74 | 75 | test('bytes.free()', (t) => { 76 | const bytes = new MuBytes(); 77 | bytes.free(new Uint8Array(1)); 78 | bytes.free(new Uint8Array(2)); 79 | bytes.free(new Uint8Array(2)); 80 | t.equal(bytes.pool[1].length, 1); 81 | t.equal(bytes.pool[2].length, 2); 82 | t.end(); 83 | }); 84 | -------------------------------------------------------------------------------- /src/schema/test/muType.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { 3 | MuVoid, 4 | MuBoolean, 5 | MuASCII, 6 | MuFixedASCII, 7 | MuUTF8, 8 | MuFloat32, 9 | MuFloat64, 10 | MuInt8, 11 | MuInt16, 12 | MuInt32, 13 | MuUint8, 14 | MuUint16, 15 | MuUint32, 16 | MuArray, 17 | MuOption, 18 | MuSortedArray, 19 | MuStruct, 20 | MuUnion, 21 | MuBytes, 22 | MuDictionary, 23 | MuVector, 24 | MuDate, 25 | MuJSON, 26 | } from '../index'; 27 | 28 | test('schema.muType', (t) => { 29 | t.equal(new MuVoid().muType, 'void'); 30 | t.equal(new MuBoolean().muType, 'boolean'); 31 | t.equal(new MuASCII().muType, 'ascii'); 32 | t.equal(new MuFixedASCII(0).muType, 'fixed-ascii'); 33 | t.equal(new MuUTF8().muType, 'utf8'); 34 | t.equal(new MuFloat32().muType, 'float32'); 35 | t.equal(new MuFloat64().muType, 'float64'); 36 | t.equal(new MuInt8().muType, 'int8'); 37 | t.equal(new MuInt16().muType, 'int16'); 38 | t.equal(new MuInt32().muType, 'int32'); 39 | t.equal(new MuUint8().muType, 'uint8'); 40 | t.equal(new MuUint16().muType, 'uint16'); 41 | t.equal(new MuUint32().muType, 'uint32'); 42 | t.equal(new MuDate().muType, 'date'); 43 | t.equal(new MuJSON().muType, 'json'); 44 | t.equal(new MuArray(new MuFloat32(), 0).muType, 'array'); 45 | t.equal(new MuSortedArray(new MuFloat32(), 0).muType, 'sorted-array'); 46 | t.equal(new MuStruct({}).muType, 'struct'); 47 | t.equal(new MuUnion({ f: new MuFloat32() }).muType, 'union'); 48 | t.equal(new MuBytes(new Uint8Array(1)).muType, 'bytes'); 49 | t.equal(new MuDictionary(new MuFloat32(), 0).muType, 'dictionary'); 50 | t.equal(new MuVector(new MuFloat32(), 5).muType, 'vector'); 51 | t.equal(new MuOption(new MuFloat32()).muType, 'option'); 52 | t.end(); 53 | }); 54 | -------------------------------------------------------------------------------- /src/schema/test/stats.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { MuWriteStream, MuReadStream } from '../../stream'; 3 | import { MuStruct } from '../index'; 4 | 5 | test('struct.stats()', (t) => { 6 | t.test('alloc, alloc, free, free', (st) => { 7 | const struct = new MuStruct({}); 8 | let stats = struct.stats(); 9 | st.equal(stats.allocCount, 0); 10 | st.equal(stats.freeCount, 0); 11 | st.equal(stats.poolSize, 0); 12 | 13 | const s1 = struct.alloc(); 14 | stats = struct.stats(); 15 | st.equal(stats.allocCount, 1); 16 | st.equal(stats.freeCount, 0); 17 | st.equal(stats.poolSize, 0); 18 | 19 | const s2 = struct.alloc(); 20 | stats = struct.stats(); 21 | st.equal(stats.allocCount, 2); 22 | st.equal(stats.freeCount, 0); 23 | st.equal(stats.poolSize, 0); 24 | 25 | struct.free(s1); 26 | stats = struct.stats(); 27 | st.equal(stats.allocCount, 2); 28 | st.equal(stats.freeCount, 1); 29 | st.equal(stats.poolSize, 1); 30 | 31 | struct.free(s2); 32 | stats = struct.stats(); 33 | st.equal(stats.allocCount, 2); 34 | st.equal(stats.freeCount, 2); 35 | st.equal(stats.poolSize, 2); 36 | st.end(); 37 | }); 38 | 39 | t.test('alloc, alloc, free, patch, free', (st) => { 40 | const struct = new MuStruct({}); 41 | let stats = struct.stats(); 42 | t.equal(stats.allocCount, 0); 43 | t.equal(stats.freeCount, 0); 44 | t.equal(stats.poolSize, 0); 45 | 46 | const s1 = struct.alloc(); 47 | const s2 = struct.alloc(); 48 | const out = new MuWriteStream(1); 49 | struct.diff(s1, s2, out); 50 | struct.free(s2); 51 | const inp = new MuReadStream(out.buffer.uint8); 52 | const s3 = struct.patch(s1, inp); 53 | stats = struct.stats(); 54 | t.equal(stats.allocCount, 3); 55 | t.equal(stats.freeCount, 1); 56 | t.equal(stats.poolSize, 0); 57 | 58 | struct.free(s1); 59 | stats = struct.stats(); 60 | t.equal(stats.allocCount, 3); 61 | t.equal(stats.freeCount, 2); 62 | t.equal(stats.poolSize, 1); 63 | st.end(); 64 | }); 65 | 66 | t.test('alloc, clone', (st) => { 67 | const struct = new MuStruct({}); 68 | let stats = struct.stats(); 69 | st.equal(stats.allocCount, 0); 70 | st.equal(stats.freeCount, 0); 71 | st.equal(stats.poolSize, 0); 72 | 73 | struct.clone(struct.alloc()); 74 | stats = struct.stats(); 75 | st.equal(stats.allocCount, 2); 76 | st.equal(stats.freeCount, 0); 77 | st.equal(stats.poolSize, 0); 78 | st.end(); 79 | }); 80 | 81 | t.end(); 82 | }); 83 | -------------------------------------------------------------------------------- /src/schema/uint16.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuUint16 extends MuNumber<'uint16'> { 5 | constructor(identity?:number) { 6 | super(identity, 'uint16'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(2); 12 | out.writeUint16(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readUint16(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/uint32.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuUint32 extends MuNumber<'uint32'> { 5 | constructor(identity?:number) { 6 | super(identity, 'uint32'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(4); 12 | out.writeUint32(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readUint32(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/uint8.ts: -------------------------------------------------------------------------------- 1 | import { MuWriteStream, MuReadStream } from '../stream'; 2 | import { MuNumber } from './_number'; 3 | 4 | export class MuUint8 extends MuNumber<'uint8'> { 5 | constructor(identity?:number) { 6 | super(identity, 'uint8'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(1); 12 | out.writeUint8(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readUint8(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/utf8.ts: -------------------------------------------------------------------------------- 1 | import { MuReadStream, MuWriteStream } from '../stream'; 2 | import { MuString } from './_string'; 3 | 4 | export class MuUTF8 extends MuString<'utf8'> { 5 | constructor (identity?:string) { 6 | super(identity || '', 'utf8'); 7 | } 8 | 9 | public diff (base:string, target:string, stream:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | stream.writeString(target); 12 | return true; 13 | } 14 | return false; 15 | } 16 | 17 | public patch (base:string, stream:MuReadStream) : string { 18 | return stream.readString(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/schema/varint.ts: -------------------------------------------------------------------------------- 1 | import { MuNumber } from './_number'; 2 | import { MuWriteStream, MuReadStream } from '../stream'; 3 | 4 | export class MuVarint extends MuNumber<'varint'> { 5 | constructor (identity?:number) { 6 | super(identity, 'varint'); 7 | } 8 | 9 | public diff (base:number, target:number, out:MuWriteStream) : boolean { 10 | if (base !== target) { 11 | out.grow(5); 12 | out.writeVarint(target); 13 | return true; 14 | } 15 | return false; 16 | } 17 | 18 | public patch (base:number, inp:MuReadStream) : number { 19 | return inp.readVarint(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/void.ts: -------------------------------------------------------------------------------- 1 | import { MuSchema } from './schema'; 2 | import { MuWriteStream, MuReadStream } from '../stream'; 3 | 4 | /** The empty type */ 5 | export class MuVoid implements MuSchema { 6 | public readonly identity:void = undefined; 7 | public readonly muType = 'void'; 8 | public readonly json = { 9 | type: 'void', 10 | }; 11 | 12 | public alloc () : void { } 13 | public free (_:void) : void { } 14 | public equal (a:void, b:void) : true { return true; } 15 | public clone (_:void) : void { } 16 | public assign (d:void, s:void) : void { } 17 | public diff (b, t, out:MuWriteStream) : false { return false; } 18 | public patch (b, inp:MuReadStream) : void { } 19 | public toJSON (_:void) : null { return null; } 20 | public fromJSON (_:null) : void { return; } 21 | } 22 | -------------------------------------------------------------------------------- /src/socket/README.md: -------------------------------------------------------------------------------- 1 | # socket 2 | a collection of socket-server modules each supporting a commonly used communication protocol, and both reliable and unreliable delivery 3 | 4 | Each `socket` module exports a class implementing the `MuSocket` interface (usually an adapter) and a class implementing the `MuSocketServer` interfaces. And they can be used out of the box with `MuClient` and `MuServer` respectively. 5 | 6 | ## `MuSocket` 7 | The interface of both client-side and server-side sockets. 8 | 9 | ### props 10 | ```ts 11 | sessionId:string 12 | ``` 13 | User-generated session id. 14 | 15 | ```ts 16 | state:MuSocketState 17 | 18 | enum MuSocketState { INIT, OPEN, CLOSED } 19 | ``` 20 | Initial state should be `INIT`. 21 | 22 | A `MuSocket` should maintain simple state machine with the following transitions. 23 | 24 | | | INIT | OPEN | CLOSED | 25 | |---------|--------|--------|--------| 26 | | open() | OPEN | ERROR | ERROR | 27 | | close() | CLOSED | CLOSED | CLOSED | 28 | 29 | ### methods 30 | ```ts 31 | open(spec:{ 32 | ready:() => void, 33 | message:(data:Uint8Array|string, ) => void, 34 | close:(error?:any) => void, 35 | }) : void 36 | ``` 37 | Connects to the socket server and establishes at least one reliable and one unreliable delivery channel, and hooks the handlers. 38 | * `ready()` is called when it's ready to receive data 39 | * `message()` is called when data is received 40 | * `close()` is called when the socket is being closed 41 | 42 | ```ts 43 | send(data:Uint8Array|string, unreliable?:boolean) : void 44 | ``` 45 | Sends data to the socket server. `unreliable` is used to determine whether to use reliable or unreliable delivery. 46 | 47 | ```ts 48 | close() : void 49 | ``` 50 | Closes the connection. 51 | 52 | ## `MuSocketServer` 53 | 54 | ### props 55 | ```ts 56 | state:MuSocketServerState 57 | 58 | enum MuSocketServerState { 59 | INIT, 60 | RUNNING, 61 | SHUTDOWN, 62 | } 63 | ``` 64 | Initial state should be `INIT`. 65 | 66 | A `MuSocketServer` should maintain a simple state machine with the following transitions. 67 | 68 | | | INIT | RUNNING | SHUTDOWN | 69 | | ------- | -------- | -------- | -------- | 70 | | start() | RUNNING | ERROR | ERROR | 71 | | close() | SHUTDOWN | SHUTDOWN | SHUTDOWN | 72 | 73 | ```ts 74 | clients:MuSocket[] 75 | ``` 76 | A list of open connections. When a connection is closed, it should be removed from the list. 77 | 78 | ### methods 79 | ```ts 80 | start(spec:{ 81 | ready:() => void, 82 | connection:(socket:MuSocket) => void, 83 | close:(error?:any) => void, 84 | }) 85 | ``` 86 | Spins up a server and hooks the handlers. 87 | * `ready()` is called when the socket server is ready to handle connections 88 | * `connection()` is called when a connection is established 89 | * `close()` is called when the server is being shut down 90 | 91 | ```ts 92 | close() : void 93 | ``` 94 | Closes all connections and shuts down the socket server. 95 | -------------------------------------------------------------------------------- /src/socket/debug/README.md: -------------------------------------------------------------------------------- 1 | # mudebug-socket 2 | A wrapper of any `mudb` sockets and socket servers for simulating network conditions. 3 | 4 | ## usage 5 | 6 | To simulate network conditions on the client side. 7 | 8 | ```js 9 | // downstream latency of ~100 ms 10 | new MuDebugSocket({ 11 | socket, // a MuSocket 12 | inLatency: 100, 13 | }) 14 | 15 | // downstream jitter up to 30 ms 16 | new MuDebugSocket({ 17 | socket, 18 | inJitter: 30, 19 | }) 20 | 21 | // ~1% downstream packet loss 22 | new MuDebugSocket({ 23 | socket, 24 | inPacketLoss: 1, 25 | }) 26 | 27 | // upstream latency of ~100 ms 28 | new MuDebugSocket({ 29 | socket, 30 | outLatency: 100, 31 | }) 32 | 33 | // upstream jitter up to 30 ms 34 | new MuDebugSocket({ 35 | socket, 36 | outJitter: 30, 37 | }) 38 | 39 | // ~1% upstream packet loss 40 | new MuDebugSocket({ 41 | socket, 42 | outPacketLoss: 1, 43 | }) 44 | ``` 45 | 46 | You can pass in any combinations of the above to simulate the condition that you need. Also, you can simulate the same set of network conditions on the server side. 47 | 48 | Unlike in other `mudb` socket modules, `MuDebugSocket` and `MuDebugServer` are designed to be **asymmetrical**, meaning you don't have to use both at the same time. 49 | 50 | ## table of contents 51 | 52 | * [2 api](#section_2) 53 | * [2.1 class: MuDebugServer](#section_2.1) 54 | * [2.1.1 new MuDebugServer(spec)](#section_2.1.1) 55 | * [2.2 class: MuDebugSocket](#section_2.2) 56 | * [2.2.1 new MuDebugSocket(spec)](#section_2.2.1) 57 | * [3 TODO](#section_3) 58 | 59 | ## 2 api 60 | 61 | ### 2.1 class: MuDebugServer 62 | 63 | #### 2.1.1 new MuDebugServer(spec) 64 | * `spec:object` 65 | * `socketServer:MuSocketServer` 66 | * `inLatency?:number` 67 | * `inJitter?:number` 68 | * `inPacketLoss?:number` 69 | * `outLatency?:number` 70 | * `outJitter?:number` 71 | * `outPacketLoss?:number` 72 | 73 | ### 2.2 class: MuDebugSocket 74 | 75 | #### 2.2.1 new MuDebugSocket(spec) 76 | * `spec:object` 77 | * `socket:MuSocket` 78 | * `inLatency?:number` 79 | * `inJitter?:number` 80 | * `inPacketLoss?:number` 81 | * `outLatency?:number` 82 | * `outJitter?:number` 83 | * `outPacketLoss?:number` 84 | 85 | ## 3 TODO 86 | * **tampered packets** 87 | * low bandwidth 88 | * slow open 89 | * out-of-order packets 90 | * duplicate packets 91 | * throttling 92 | -------------------------------------------------------------------------------- /src/socket/debug/test/_/schema.ts: -------------------------------------------------------------------------------- 1 | import { MuUint32 } from '../../../../schema'; 2 | 3 | export const protocolSchema = { 4 | client: { 5 | pong: new MuUint32(), 6 | }, 7 | server: { 8 | ping: new MuUint32(), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/socket/debug/test/web.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import * as http from 'http'; 3 | 4 | import { MuWebSocketServer } from '../../web/server'; 5 | import { MuDebugServer } from '../index'; 6 | import { MuServer } from '../../../server'; 7 | import { MuWebSocket } from '../../web/client'; 8 | import { MuDebugSocket } from '../index'; 9 | import { MuClient } from '../../../client'; 10 | import { protocolSchema } from './_/schema'; 11 | import { findPort } from '../../../util/port'; 12 | 13 | function randStr () { 14 | return Math.random().toString(36).substring(2); 15 | } 16 | 17 | test.onFinish(() => process.exit(0)); 18 | (test).onFailure(() => process.exit(1)); 19 | 20 | const PING_OPCODE = 0x9; 21 | const PONG_OPCODE = 0xA; 22 | 23 | const server = http.createServer(); 24 | const socketServer = new MuWebSocketServer({ server }); 25 | const debugSocketServer = new MuDebugServer({ socketServer }); 26 | const muServer = new MuServer(debugSocketServer); 27 | muServer.protocol(protocolSchema).configure({ 28 | message: { 29 | ping: (client, data, unreliable) => { 30 | const response = data === PING_OPCODE ? PONG_OPCODE : data; 31 | client.message.pong(response, !!unreliable); 32 | }, 33 | }, 34 | }); 35 | muServer.start(); 36 | 37 | findPort((port) => { 38 | server.listen(port); 39 | 40 | const url = `ws://127.0.0.1:${port}`; 41 | 42 | test('WebSocket - simulating network latency on client side', (t) => { 43 | t.plan(2); 44 | 45 | let timestamp:number; 46 | const upstreamLatency = 75; 47 | const downstreamLatency = 25; 48 | 49 | const socket = new MuWebSocket({ 50 | sessionId: randStr(), 51 | url, 52 | }); 53 | const debugSocket = new MuDebugSocket({ 54 | socket, 55 | outLatency: upstreamLatency, 56 | inLatency: downstreamLatency, 57 | }); 58 | const muClient = new MuClient(debugSocket); 59 | 60 | const clientProtocol = muClient.protocol(protocolSchema); 61 | clientProtocol.configure({ 62 | message: { 63 | pong: (data) => { 64 | const delay = Date.now() - timestamp; 65 | const latency = upstreamLatency + downstreamLatency; 66 | 67 | t.equal(data, PONG_OPCODE); 68 | t.ok(delay >= latency, `client should receive opcode after >=${latency} ms`); 69 | }, 70 | }, 71 | }); 72 | 73 | muClient.start({ 74 | ready: () => { 75 | clientProtocol.server.message.ping(PING_OPCODE); 76 | timestamp = Date.now(); 77 | }, 78 | }); 79 | }); 80 | 81 | test('WebSocket - maintaining order of messages from client side', (t) => { 82 | let head = 10; 83 | const end = 64; 84 | 85 | t.plan(end - head); 86 | 87 | const socket = new MuWebSocket({ 88 | sessionId: randStr(), 89 | url, 90 | }); 91 | const debugSocket = new MuDebugSocket({ 92 | socket, 93 | outLatency: 75, 94 | outJitter: 10, 95 | inLatency: 25, 96 | inJitter: 10, 97 | }); 98 | const muClient = new MuClient(debugSocket); 99 | 100 | const clientProtocol = muClient.protocol(protocolSchema); 101 | clientProtocol.configure({ 102 | message: { 103 | pong: (data) => { 104 | t.equal(data, head++); 105 | }, 106 | }, 107 | }); 108 | 109 | muClient.start({ 110 | ready: () => { 111 | for (let i = head; i < end; ++i) { 112 | clientProtocol.server.message.ping(i); 113 | } 114 | }, 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/socket/local/README.md: -------------------------------------------------------------------------------- 1 | # local-socket 2 | network socket emulation for development purpose 3 | 4 | In `local-socket`, no real network connections are established so no Web servers are needed, meaning applications using `local-socket` can run entirely in a browser. It can make your life easier: 5 | * you can forget about restarting the server on changes 6 | * you can debug using the dev tools provided by browsers 7 | 8 | ## example 9 | 10 | ```ts 11 | import { MuServer, MuClient } from 'mudb' 12 | import { createLocalSocketServer, createLocalSocket } from 'mudb/socket/local' 13 | 14 | const socketServer = createLocalSocketServer() 15 | const server = new MuServer(socketServer) 16 | 17 | const socket = createLocalSocket({ 18 | sessionId: Math.random().toString(16).substring(2), 19 | server: socketServer, 20 | }) 21 | const client = new MuClient(socket) 22 | ``` 23 | 24 | ## API 25 | 26 | * [`createLocalSocketServer()`](#createlocalsocketserver()) 27 | * [`createLocalSocket()`](#createlocalsocket()) 28 | * [`MuLocalSocketServer`](#mulocalsocketserver) 29 | * [`MuLocalSocket`](#mulocalsocket) 30 | 31 | Use the factory functions instead of the constructors. 32 | 33 | --- 34 | 35 | ### `createLocalSocketServer()` 36 | Creates a pseudo socket server. 37 | 38 | ```ts 39 | createLocalSocketServer(spec?:{ 40 | scheduler?:MuScheduler, 41 | }) : MuLocalSocketServer 42 | ``` 43 | * `scheduler` can be set to a [`MuMockScheduler`](../../scheduler/README#mumockscheduler) for testing 44 | 45 | --- 46 | 47 | ### `createLocalSocket()` 48 | Creates a "connection" to the socket server passed in, and returns the socket. 49 | 50 | ```ts 51 | createLocalSocket(spec:{ 52 | sessionId:string, 53 | server:MuLocalSocketServer, 54 | scheduler?:MuScheduler, 55 | }) : MuLocalSocket 56 | ``` 57 | * `sessionId` a user-generated session id 58 | * `server` the server returned by `createLocalSocketServer()` 59 | * `scheduler` can be set to a [`MuMockScheduler`](../../scheduler/README#mumockscheduler) for testing 60 | 61 | --- 62 | 63 | ### `MuLocalSocketServer` 64 | implements [`MuSocketServer`](../README#musocketserver) 65 | 66 | A pseudo socket server. 67 | 68 | --- 69 | 70 | ### `MuLocalSocket` 71 | implements [`MuSocket`](../README#musocket) 72 | 73 | A pseudo socket. 74 | -------------------------------------------------------------------------------- /src/socket/local/test/connection.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | 3 | import { createLocalSocketServer, createLocalSocket } from '../index'; 4 | import { MuServer } from '../../../server'; 5 | import { MuClient } from '../../../client'; 6 | import { MuSocketState } from '../../socket'; 7 | 8 | function randomId () { 9 | return Math.random().toString(36).substring(2); 10 | } 11 | 12 | test('client socket will always be open first', (t) => { 13 | t.test('when server starts first', (st) => { 14 | st.plan(3); 15 | 16 | const socketServer = createLocalSocketServer(); 17 | const clientSocket = createLocalSocket({ 18 | sessionId: randomId(), 19 | server: socketServer, 20 | }); 21 | const serverSocket = clientSocket._duplex; 22 | 23 | const server = new MuServer(socketServer); 24 | const client = new MuClient(clientSocket); 25 | 26 | server.start({ 27 | ready: () => client.start({ 28 | ready: () => { 29 | st.equal(clientSocket.state(), MuSocketState.OPEN); 30 | st.equal(serverSocket.state(), MuSocketState.INIT); 31 | setTimeout(() => st.equal(serverSocket.state(), MuSocketState.OPEN), 0); 32 | }, 33 | }), 34 | }); 35 | }); 36 | 37 | t.test('when client starts first', (st) => { 38 | st.plan(3); 39 | 40 | const socketServer = createLocalSocketServer(); 41 | const clientSocket = createLocalSocket({ 42 | sessionId: randomId(), 43 | server: socketServer, 44 | }); 45 | const serverSocket = clientSocket._duplex; 46 | 47 | const server = new MuServer(socketServer); 48 | const client = new MuClient(clientSocket); 49 | 50 | client.start({ 51 | ready: () => server.start({ 52 | ready: () => { 53 | st.equal(clientSocket.state(), MuSocketState.OPEN); 54 | st.equal(serverSocket.state(), MuSocketState.INIT); 55 | setTimeout(() => st.equal(serverSocket.state(), MuSocketState.OPEN), 0); 56 | }, 57 | }), 58 | }); 59 | }); 60 | 61 | t.end(); 62 | }); 63 | -------------------------------------------------------------------------------- /src/socket/multiplex.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuSocketServer, 3 | MuSocketServerState, 4 | MuSocketServerSpec, 5 | MuSocket, 6 | MuCloseHandler, 7 | } from './socket'; 8 | 9 | export class MuMultiSocketServer implements MuSocketServer { 10 | private _state = MuSocketServerState.INIT; 11 | 12 | public state () : MuSocketServerState { 13 | return this._state; 14 | } 15 | 16 | // currently not in use 17 | public clients:MuSocket[] = []; 18 | 19 | private _servers:MuSocketServer[]; 20 | private _numLiveServers = 0; 21 | 22 | private _onclose:MuCloseHandler = () => { }; 23 | 24 | constructor (servers:MuSocketServer[]) { 25 | this._servers = servers; 26 | } 27 | 28 | public start (spec:MuSocketServerSpec) { 29 | this._onclose = spec.close; 30 | 31 | for (let i = 0; i < this._servers.length; ++i) { 32 | this._servers[i].start({ 33 | ready: () => ++this._numLiveServers, 34 | connection: spec.connection, 35 | close: () => --this._numLiveServers, 36 | }); 37 | } 38 | 39 | const startHandle = setInterval( 40 | () => { 41 | if (this._numLiveServers < this._servers.length) { 42 | return; 43 | } 44 | 45 | clearInterval(startHandle); 46 | 47 | this._state = MuSocketServerState.RUNNING; 48 | spec.ready(); 49 | }, 50 | 300, 51 | ); 52 | } 53 | 54 | public close () { 55 | for (let i = 0; i < this._servers.length; ++i) { 56 | this._servers[i].close(); 57 | } 58 | 59 | const closeHandle = setInterval( 60 | () => { 61 | if (this._numLiveServers > 0) { 62 | return; 63 | } 64 | 65 | clearInterval(closeHandle); 66 | 67 | this._state = MuSocketServerState.SHUTDOWN; 68 | this._onclose(); 69 | }, 70 | 300, 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/socket/net/README.md: -------------------------------------------------------------------------------- 1 | # munet-socket 2 | TCP/UDP communications made available for `mudb`, useful when building apps with frameworks like Electron. 3 | 4 | # usage 5 | 6 | **server** 7 | 8 | ```js 9 | var tcp = require('net') 10 | var udp = require('dgram') 11 | 12 | var MuNetSocketServer = require('munet-socket/server').MuNetSocketServer 13 | var MuServer = require('mudb/server').MuServer 14 | 15 | var tcpServer = tcp.createServer() 16 | var udpServer = udp.createSocket({ 17 | type: 'udp4', 18 | reuseAddr: true, 19 | }) 20 | var socketServer = new MuNetSocketServer({ 21 | tcpServer, 22 | udpServer, 23 | }) 24 | var muServer = new MuServer(socketServer) 25 | 26 | tcpServer.listen(9977) 27 | udpServer.bind(9988, '127.0.0.1') 28 | muServer.start() 29 | ``` 30 | 31 | **client** 32 | 33 | ```js 34 | var tcp = require('net') 35 | var udp = require('dgram') 36 | 37 | var MuNetSocket = require('munet-socket/socket').MuNetSocket 38 | var MuClient = require('mudb/client').MuClient 39 | 40 | var socket = new MuNetSocket({ 41 | sessionId: Math.random().toString(36).substr(2), 42 | 43 | // for TCP socket 44 | connectOpts: { 45 | port: 9977, 46 | host: '127.0.0.1', 47 | }, 48 | 49 | // for UDP socket 50 | bindOpts: { 51 | port: 9989, 52 | address: '127.0.0.1', 53 | }, 54 | }) 55 | var muClient = new MuClient(socket) 56 | 57 | muClient.start() 58 | ``` 59 | 60 | # table of contents 61 | 62 | * [2 api](#section_2) 63 | * [2.1 class: MuNetSocketServer](#section_2.1) 64 | * [2.1.1 new MuNetSocketServer(spec)](#section_2.1.1) 65 | * [2.2 class: MuNetSocket](#section_2.2) 66 | * [2.2.1 new MuNetSocket(spec)](#section_2.2.1) 67 | 68 | # 2 api 69 | 70 | ## 2.1 class: MuNetSocketServer 71 | 72 | ### 2.1.1 new MuNetSocketServer(spec) 73 | * `spec` `` 74 | * `tcpServer` `` the underlying TCP server 75 | * `udpServer` `` the underlying UDP server 76 | 77 | ## 2.2 class: MuNetSocket 78 | 79 | ### 2.2.1 new MuNetSocket(spec) 80 | * `spec` `` 81 | * `sessionId` `` a unique session id used to identify the client 82 | * `connectOpts` `` used by [connect()](https://nodejs.org/dist/latest/docs/api/net.html#net_socket_connect_options_connectlistener) to initiate a connection when the client starts 83 | * `bindOpts` `` used by [bind()](https://nodejs.org/dist/latest/docs/api/dgram.html#dgram_socket_bind_options_callback) to make the socket listen for datagram messages when the client starts 84 | * `tcpSocket` `` optional 85 | * `udpSocket` `` optional 86 | -------------------------------------------------------------------------------- /src/socket/net/util.ts: -------------------------------------------------------------------------------- 1 | import { MuData } from '../socket'; 2 | 3 | enum Read { 4 | HEADER, 5 | PAYLOAD, 6 | } 7 | 8 | const HEADER_LENGTH = 4; 9 | 10 | // TODO minimize buffer allocation 11 | export function messagify (socket) { 12 | let bufSize = 0; 13 | const buf:Buffer[] = []; 14 | 15 | let hasMsg = false; 16 | let next = Read.HEADER; 17 | let msgLength = 0; 18 | 19 | function readBuf (length:number) : Buffer { 20 | bufSize -= length; 21 | 22 | if (length === buf[0].length) { 23 | return buf.shift() as Buffer; 24 | } 25 | 26 | let result:Buffer; 27 | if (length < buf[0].length) { 28 | result = buf[0].slice(0, length); 29 | buf[0] = buf[0].slice(length); 30 | return result; 31 | } 32 | 33 | result = Buffer.allocUnsafe(length); 34 | let offset = 0; 35 | let bl; 36 | while (length > 0) { 37 | bl = buf[0].length; 38 | 39 | if (length >= bl) { 40 | buf[0].copy(result, offset); 41 | offset += bl; 42 | buf.shift(); 43 | } else { 44 | buf[0].copy(result, offset, 0, length); 45 | buf[0] = buf[0].slice(length); 46 | } 47 | 48 | length -= bl; 49 | } 50 | return result; 51 | } 52 | 53 | function fetchMsg () { 54 | while (hasMsg) { 55 | if (next === Read.HEADER) { 56 | if (bufSize >= HEADER_LENGTH) { 57 | next = Read.PAYLOAD; 58 | msgLength = readBuf(HEADER_LENGTH).readUInt32LE(0); 59 | } else { 60 | hasMsg = false; 61 | } 62 | } else if (next === Read.PAYLOAD) { 63 | if (bufSize >= msgLength) { 64 | next = Read.HEADER; 65 | socket.emit('message', readBuf(msgLength)); 66 | } else { 67 | hasMsg = false; 68 | } 69 | } 70 | } 71 | } 72 | 73 | const socketWrite = socket.write.bind(socket); 74 | socket.write = function (data:MuData) { 75 | const payload = Buffer.from(data); 76 | const header = Buffer.allocUnsafe(HEADER_LENGTH); 77 | header.writeUInt32LE(payload.length, 0); 78 | 79 | socketWrite(header); 80 | socketWrite(payload); 81 | }; 82 | 83 | socket.on('data', (data) => { 84 | buf.push(data); 85 | bufSize += data.length; 86 | 87 | hasMsg = true; 88 | fetchMsg(); 89 | }); 90 | } 91 | 92 | export function isJSON (buf:Buffer) { 93 | return buf[0] === 0x7b && buf[buf.length - 1] === 0x7d; 94 | } 95 | -------------------------------------------------------------------------------- /src/socket/socket.ts: -------------------------------------------------------------------------------- 1 | export type MuSessionId = string; 2 | 3 | export type MuData = Uint8Array | string; 4 | 5 | export interface MuReadyHandler { 6 | () : void; 7 | } 8 | export interface MuMessageHandler { 9 | (data:MuData, unreliable:boolean) : void; 10 | } 11 | export interface MuCloseHandler { 12 | (error?:any) : void; 13 | } 14 | export interface MuConnectionHandler { 15 | (socket:MuSocket) : void; 16 | } 17 | 18 | export enum MuSocketState { 19 | INIT, 20 | OPEN, 21 | CLOSED, 22 | } 23 | 24 | export interface MuSocketSpec { 25 | ready:MuReadyHandler; 26 | message:MuMessageHandler; 27 | close:MuCloseHandler; 28 | } 29 | 30 | export interface MuSocket { 31 | readonly sessionId:MuSessionId; 32 | state() : MuSocketState; 33 | open(spec:MuSocketSpec); 34 | send(data:MuData, unreliable?:boolean); 35 | close(); 36 | reliableBufferedAmount() : number; 37 | unreliableBufferedAmount() : number; 38 | } 39 | 40 | export enum MuSocketServerState { 41 | INIT, 42 | RUNNING, 43 | SHUTDOWN, 44 | } 45 | 46 | export interface MuSocketServerSpec { 47 | ready:MuReadyHandler; 48 | connection:MuConnectionHandler; 49 | close:MuCloseHandler; 50 | } 51 | 52 | export interface MuSocketServer { 53 | clients:MuSocket[]; 54 | state() : MuSocketServerState; 55 | start(spec:MuSocketServerSpec); 56 | close(); 57 | } 58 | -------------------------------------------------------------------------------- /src/socket/web/README.md: -------------------------------------------------------------------------------- 1 | # web-socket 2 | for WebSocket communications, server implementation based on [`ws`](https://github.com/websockets/ws) 3 | 4 | ## example 5 | 6 | **server** 7 | 8 | ```ts 9 | import { MuWebSocketServer } from 'mudb/socket/web/server' 10 | import { MuServer } from 'mudb/server' 11 | import http = require('http') 12 | 13 | const httpServer = http.createServer() 14 | const socketServer = new MuWebSocketServer({ 15 | server: httpServer, 16 | }) 17 | const server = new MuServer(socketServer) 18 | ``` 19 | 20 | **client** 21 | 22 | ```ts 23 | import { MuWebSocket } from 'mudb/socket/web/client' 24 | import { MuClient } from 'mudb/client' 25 | 26 | const socket = new MuWebSocket({ 27 | sessionId: Math.random().toString(36).substring(2), 28 | url: 'ws://127.0.0.1:9966', 29 | }) 30 | const client = new MuClient(socket) 31 | ``` 32 | 33 | ## API 34 | * [`MuWebSocketServer`](#muwebsocketserver) 35 | * [`MuWebSocket`](#muwebsocket) 36 | 37 | --- 38 | 39 | ### `MuWebSocketServer` 40 | implements [`MuSocketServer`](../README#musocketserver) 41 | 42 | ```ts 43 | import { MuWebSocketServer } from 'mudb/socket/web/server' 44 | 45 | new MuWebSocketServer(spec:{ 46 | server:http.Server|https.Server, 47 | bufferLimit:number=1024, 48 | pingInterval:number=0, 49 | backlog?:number, 50 | maxPayload?:number, 51 | path?:string, 52 | handleProtocols?:(protocols:any[], request:http.IncomingMessage) => any, 53 | perMessageDeflate:boolean|object=false, 54 | scheduler?:MuScheduler, 55 | logger?:MuLogger; 56 | }) 57 | ``` 58 | * `server` an HTTP/S server 59 | * `bufferLimit` the hard limit on the byte size of buffered data per connection, exceeding which will cause unreliable messages to be dropped 60 | * `pingInterval` if >0, server will send a ping frame to an idle connection every `pingInterval` ms 61 | * `backlog` the hard limit on the number of pending connections 62 | * `maxPayload` the maximum byte size of each message 63 | * `path` if specified, only connections matching `path` will be accepted 64 | * `handleProtocols(protocols, request)` a function used to handle the WebSocket subprotocols 65 | * `protocols` a list of subprotocols indicated by the client in the `Sec-WebSocket-Protocol` header 66 | * `request` an HTTP GET request 67 | * `perMessageDeflate` controlling the behavior of [permessage-deflate extension](https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19#page-15), disabled by default, can be a table of extension parameters: 68 | * `serverNoContextTakeover:boolean` whether to include the `server_no_context_takeover` parameter in the corresponding 69 | negotiation response 70 | * `clientNoContextTakeover:boolean` whether to include the `client_no_context_takeover` parameter in the corresponding 71 | negotiation response 72 | * `serverMaxWindowBits:number` the value of `windowBits` 73 | * `clientMaxWindowBits:number` request a custom client window size 74 | * `threshold:number=1024` payloads smaller than this will not be compressed 75 | * `zlibDeflateOptions:object` [options](https://nodejs.org/api/zlib.html#zlib_class_options) to pass to zlib on deflate 76 | * `zlibInflateOptions:object` [options](https://nodejs.org/api/zlib.html#zlib_class_options) to pass to zlib on inflate 77 | * `scheduler` can be set to a [`MuMockScheduler`](../../scheduler/README#mumockscheduler) for testing 78 | 79 | --- 80 | 81 | ### `MuWebSocket` 82 | implements [`MuSocket`](../README#musocket) 83 | 84 | ```ts 85 | import { MuWebSocket } from 'mudb/socket/web/client' 86 | 87 | new MuWebSocket(spec:{ 88 | sessionId:string, 89 | url:string, 90 | maxSockets:number=5, 91 | bufferLimit:number=1024, 92 | logger?:MuLogger, 93 | }) 94 | ``` 95 | * `sessionId` the token used to identify the client 96 | * `url` the server URL 97 | * `maxSockets` the number of WebSocket connections to be opened 98 | * `bufferLimit` the hard limit on the byte size of buffered data, exceeding which will cause unreliable messages to be dropped 99 | -------------------------------------------------------------------------------- /src/socket/webrtc/README.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { MuRTCSocketServer } from 'mudb/socket/webrtc/server'; 3 | import { MuRTCSocket } from 'mudb/socket/webrtc/client'; 4 | 5 | const socketServer = new MuRTCSocketServer({ 6 | // in Node.js 7 | // wrtc: require('wrtc'), 8 | signal: (data, sessionId) => { 9 | socket.handleSignal(data); 10 | }, 11 | }); 12 | 13 | const socket = new MuRTCSocket({ 14 | // in Node.js 15 | // wrtc: require('wrtc'), 16 | sessionId, 17 | signal: (data) => { 18 | socketServer.handleSignal(data); 19 | }, 20 | }); 21 | ``` 22 | 23 | ```ts 24 | // say we use WebSocket for signaling 25 | 26 | // server side 27 | const signalingServer = new WS.Server(); 28 | const signalingChannels = {}; 29 | const socketServer = new MuRTCSocketServer({ 30 | signal: (data, sessionId) => { 31 | signalingChannels[sessionId].send(JSON.stringify(data)); 32 | }, 33 | }); 34 | signalingServer.on('connection', (channel) => { 35 | let sessionId; 36 | channel.onmessage = ({ data }) => { 37 | channel.onmessage = ({ data }) => { 38 | socketServer.handleSignal(JSON.parse(data)); 39 | } 40 | sessionId = JSON.parse(data).sid; 41 | signalingChannels[sessionId] = channel; 42 | }; 43 | channel.onclose = () => { 44 | delete signalingChannels[sessionId]; 45 | } 46 | }); 47 | 48 | // client side 49 | const signalingChannel = new WebSocket(signalingServerURL); 50 | const socket = new MuRTCSocket({ 51 | sessionId, 52 | signal: (data) => { 53 | signalingChannel.send(JSON.stringify(data)); 54 | }; 55 | }); 56 | signalingChannel.onopen = () => { 57 | signalingChannel.send(JSON.stringify({ sid: sessionId })); 58 | }; 59 | signalingChannel.onmessage = ({ data }) => { 60 | socket.handleSignal(JSON.parse(data)); 61 | }; 62 | const client = new MuClient(socket); 63 | client.start({ 64 | ready: () => { 65 | signalingChannel.close(); 66 | } 67 | }); 68 | ``` 69 | -------------------------------------------------------------------------------- /src/socket/webrtc/rtc.ts: -------------------------------------------------------------------------------- 1 | export interface MuRTCBinding { 2 | RTCPeerConnection:typeof RTCPeerConnection; 3 | RTCSessionDescription:typeof RTCSessionDescription; 4 | RTCIceCandidate:typeof RTCIceCandidate; 5 | } 6 | 7 | export function browserRTC () : MuRTCBinding|null { 8 | if (typeof window === 'undefined') { 9 | return null; 10 | } 11 | 12 | const rtc = { 13 | RTCPeerConnection: window['RTCPeerConnection'] || window['webkitRTCPeerConnection'] || window['mozRTCPeerConnection'], 14 | RTCSessionDescription: window['RTCSessionDescription'] || window['mozRTCSessionDescription'], 15 | RTCIceCandidate: window['RTCIceCandidate'] || window['mozRTCIceCandidate'], 16 | }; 17 | if (rtc.RTCPeerConnection && rtc.RTCSessionDescription && rtc.RTCIceCandidate) { 18 | return rtc; 19 | } 20 | return null; 21 | } 22 | 23 | export interface MuRTCConfiguration extends RTCConfiguration { 24 | sdpSemantics?:string; 25 | } 26 | 27 | export interface MuRTCOfferAnswerOptions extends RTCOfferAnswerOptions { 28 | iceRestart?:boolean; 29 | } 30 | -------------------------------------------------------------------------------- /src/socket/worker/README.md: -------------------------------------------------------------------------------- 1 | # muworker-socket 2 | [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API#Web_Workers_concepts_and_usage) made available to `mudb`. Suitable for creating games with a single-player mode by running the server in a separate thread to allow better user experience. 3 | 4 | # example 5 | Both `browserify` and `webworkify` are required to run the example. 6 | 7 | **worker.js** 8 | ```js 9 | const { createWorkerSocketServer } = require('muworker-socket/server') 10 | const { MuServer } = require('mudb/server') 11 | 12 | // source of worker should go into `module.exports` 13 | module.exports = () => { 14 | const socketServer = createWorkerSocketServer() 15 | const muServer = new MuServer(socketServer) 16 | 17 | socketServer.listen() 18 | muServer.start(/* listeners */) 19 | } 20 | ``` 21 | 22 | **client.js** 23 | ```js 24 | // `webworkify` enables workers to `require()` 25 | const work = require('webworkify') 26 | const { createWorkerSocket } = require('muworker-socket/socket') 27 | const { MuClient } = require('mudb/client') 28 | 29 | const serverWorker = work(require('./worker.js')) 30 | const socket = createWorkerSocket({ 31 | sessionId: Math.random().toString(36).substr(2), 32 | serverWorker: serverWorker, 33 | }) 34 | const muClient = new MuClient(socket) 35 | 36 | muClient.start(/* listeners */) 37 | ``` 38 | 39 | # table of contents 40 | 41 | * [2 api](#section_2) 42 | * [2.1 interfaces](#section_2.1) 43 | * [2.2 `createWorkerSocketServer()`](#section_2.2) 44 | * [2.3 `createWorkerSocket(spec)`](#section_2.3) 45 | * [2.4 `MuWorkerSocketServer`](#section_2.4) 46 | * [2.4.1 `listen()`](#section_2.4.1) 47 | * [2.5 `MuWorkerSocket`](#section_2.5) 48 | 49 | # 2 api 50 | 51 | ## 2.1 interfaces 52 | 53 | Purely instructive types used to describe the API: 54 | * `SessionId`: `string` 55 | 56 | ## 2.2 `createWorkerSocketServer()` 57 | A factory returning a new instance of `MuWorkerSocketServer`. 58 | 59 | ## 2.3 `createWorkerSocket(spec)` 60 | A factory returning a new instance of `MuWorkerSocket`. 61 | 62 | * `spec:object` 63 | * `sessionId:SessionId`: a unique session id used to identify a client 64 | * `serverWorker:Worker`: the worker in which the server is running 65 | 66 | ## 2.4 `MuWorkerSocketServer` 67 | A `MuWorkerSocketServer` is a pseudo socket server that can be used to instantiate a `MuServer`. 68 | 69 | ### 2.4.1 `listen()` 70 | Starts the socket server listening for connections. 71 | 72 | ## 2.5 `MuWorkerSocket` 73 | A `MuLocalSocket` is a pseudo client-side socket that can be used to instantiate a `MuClient`. 74 | -------------------------------------------------------------------------------- /src/socket/worker/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MuSocket, 3 | MuSocketState, 4 | MuSocketSpec, 5 | MuCloseHandler, 6 | MuSessionId, 7 | MuData, 8 | } from '../socket'; 9 | 10 | function noop () { } 11 | 12 | export class MuWorkerSocket implements MuSocket { 13 | private _state = MuSocketState.INIT; 14 | public sessionId:MuSessionId; 15 | 16 | public state () { return this._state; } 17 | 18 | private _socket:Worker; 19 | 20 | private _onclose:MuCloseHandler = noop; 21 | 22 | constructor (sessionId:MuSessionId, socket:Worker) { 23 | this.sessionId = sessionId; 24 | this._socket = socket; 25 | 26 | this._socket.onerror = (ev) => { 27 | console.error(`${ev.message} (${ev.filename}:${ev.lineno})`); 28 | }; 29 | } 30 | 31 | public open (spec:MuSocketSpec) { 32 | if (this._state === MuSocketState.OPEN) { 33 | throw new Error('client-side Worker socket already open'); 34 | } 35 | if (this._state === MuSocketState.CLOSED) { 36 | throw new Error('client-side Worker socket already closed, cannot reopen'); 37 | } 38 | 39 | this._state = MuSocketState.OPEN; 40 | 41 | // perform a two-way "handshake" to ensure server is ready before sending messages 42 | // 1. client sends server the session id as a SYN 43 | // 2. server responds with the session id as an ACK 44 | 45 | this._socket.onmessage = (ev) => { 46 | if (ev.data.sessionId !== this.sessionId) { 47 | this._socket.terminate(); 48 | throw new Error('invalid ACK from server'); 49 | } 50 | 51 | // reset handler after receiving the ACK from server 52 | this._socket.onmessage = ({ data }) => { 53 | spec.message(data.message, data.unreliable); 54 | }; 55 | this._onclose = spec.close; 56 | 57 | spec.ready(); 58 | }; 59 | 60 | // send session id to server as a SYN 61 | this._socket.postMessage({ sessionId: this.sessionId }); 62 | } 63 | 64 | public send (message:MuData, unreliable_?:boolean) { 65 | if (this._state !== MuSocketState.OPEN) { 66 | return; 67 | } 68 | 69 | const unreliable = !!unreliable_; 70 | if (typeof message === 'string') { 71 | this._socket.postMessage({ 72 | message, 73 | unreliable, 74 | }); 75 | } else { 76 | this._socket.postMessage( 77 | { 78 | message, 79 | unreliable, 80 | }, 81 | [ message.buffer ], 82 | ); 83 | } 84 | } 85 | 86 | public close () { 87 | if (this._state !== MuSocketState.OPEN) { 88 | this._state = MuSocketState.CLOSED; 89 | return; 90 | } 91 | 92 | this._state = MuSocketState.CLOSED; 93 | this._socket.terminate(); 94 | this._onclose(); 95 | } 96 | 97 | public reliableBufferedAmount () { 98 | return 0; 99 | } 100 | 101 | public unreliableBufferedAmount () { 102 | return 0; 103 | } 104 | } 105 | 106 | export function createWorkerSocket (spec:{ 107 | sessionId:MuSessionId, 108 | serverWorker:Worker, 109 | }) { 110 | return new MuWorkerSocket(spec.sessionId, spec.serverWorker); 111 | } 112 | -------------------------------------------------------------------------------- /src/socket/worker/test/_/client.ts: -------------------------------------------------------------------------------- 1 | import * as work from 'webworkify'; 2 | import * as test from 'tape'; 3 | import { MuWorkerSocket } from '../../client'; 4 | import { MuSocketState } from '../../../socket'; 5 | 6 | function noop () { } 7 | 8 | function sessionId () { 9 | return Math.random().toString(36).substr(2); 10 | } 11 | 12 | function serverWorker () : Worker { 13 | return work(require('./worker')); 14 | } 15 | 16 | test('workerSocket initial state', (t) => { 17 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 18 | t.equal(socket.state(), MuSocketState.INIT, 'should be MuSocketState.INIT'); 19 | t.end(); 20 | }); 21 | 22 | test('workerSocket.open() - when INIT', (t) => { 23 | t.plan(2); 24 | 25 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 26 | socket.open({ 27 | ready: () => { 28 | t.ok(true, 'should invoke ready handler'); 29 | t.equal(socket.state(), MuSocketState.OPEN, 'should change state to MuSocketState.OPEN'); 30 | }, 31 | message: noop, 32 | close: noop, 33 | }); 34 | }); 35 | 36 | test('workerSocket.open() - when OPEN', (t) => { 37 | t.plan(1); 38 | 39 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 40 | socket.open({ 41 | ready: () => t.throws( 42 | // open socket when already open 43 | () => socket.open({ 44 | ready: noop, 45 | message: noop, 46 | close: noop, 47 | }), 48 | ), 49 | message: noop, 50 | close: noop, 51 | }); 52 | }); 53 | 54 | test('workerSocket.open() - when CLOSED', (t) => { 55 | t.plan(1); 56 | 57 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 58 | socket.open({ 59 | // close socket when open 60 | ready: () => socket.close(), 61 | message: noop, 62 | close: () => t.throws( 63 | // open socket when already closed 64 | () => socket.open({ 65 | ready: noop, 66 | message: noop, 67 | close: noop, 68 | }), 69 | ), 70 | }); 71 | }); 72 | 73 | test('workerSocket.close() - when OPEN', (t) => { 74 | t.plan(2); 75 | 76 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 77 | socket.open({ 78 | // close socket when open 79 | ready: () => socket.close(), 80 | message: noop, 81 | close: (error) => { 82 | t.equal(error, undefined, 'should invoke close handler without error message'); 83 | t.equal(socket.state(), MuSocketState.CLOSED, 'should change state to MuSocketState.CLOSED'); 84 | }, 85 | }); 86 | }); 87 | 88 | test('workerSocket.close() - when INIT', (t) => { 89 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 90 | // close socket when init 91 | socket.close(); 92 | t.equal(socket.state(), MuSocketState.CLOSED, 'should change state to MuSocketState.CLOSED'); 93 | t.end(); 94 | }); 95 | 96 | test('workerSocket.close() - when CLOSED', (t) => { 97 | const socket = new MuWorkerSocket(sessionId(), serverWorker()); 98 | socket.open({ 99 | // close socket when open 100 | ready: () => socket.close(), 101 | message: noop, 102 | close: () => { 103 | // close socket when already closed 104 | socket.close(); 105 | 106 | t.ok(true, 'should not invoke close handler'); 107 | t.end(); 108 | }, 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/socket/worker/test/_/worker.ts: -------------------------------------------------------------------------------- 1 | import { createWorkerSocketServer } from '../../server'; 2 | import { MuServer } from '../../../../server'; 3 | 4 | module.exports = () => { 5 | const socketServer = createWorkerSocketServer(); 6 | const muServer = new MuServer(socketServer); 7 | 8 | socketServer.listen(); 9 | muServer.start(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/socket/worker/test/client.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | const cwd = __dirname; 4 | 5 | spawn('browserify _/client.ts -p [ tsify ] | tape-run', [], { 6 | cwd, 7 | shell: true, 8 | stdio: 'inherit', 9 | }); 10 | -------------------------------------------------------------------------------- /src/socket/worker/test/server.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { createWorkerSocketServer } from '../server'; 3 | import { MuSocketServerState } from '../../socket'; 4 | 5 | function noop () { } 6 | 7 | test('workerSocketServer initial state', (t) => { 8 | const server = createWorkerSocketServer(); 9 | t.equal(server.state(), MuSocketServerState.INIT, 'should be MuSocketServerState.INIT'); 10 | t.end(); 11 | }); 12 | 13 | test('workerSocketServer.start() - when INIT', (t) => { 14 | t.plan(2); 15 | 16 | const server = createWorkerSocketServer(); 17 | server.start({ 18 | ready: () => { 19 | t.ok(true, 'should invoke ready handler'); 20 | t.equal(server.state(), MuSocketServerState.RUNNING, 'should change state to MuSocketServerState.RUNNING'); 21 | }, 22 | connection: noop, 23 | close: noop, 24 | }); 25 | }); 26 | 27 | test('workerSocketServer.start() - when RUNNING', (t) => { 28 | t.plan(1); 29 | 30 | const server = createWorkerSocketServer(); 31 | server.start({ 32 | ready: () => t.throws( 33 | // start server when already running 34 | () => server.start({ 35 | ready: noop, 36 | connection: noop, 37 | close: noop, 38 | }), 39 | ), 40 | connection: noop, 41 | close: noop, 42 | }); 43 | }); 44 | 45 | test('workerSocketServer.start() - when SHUTDOWN', (t) => { 46 | t.plan(1); 47 | 48 | const server = createWorkerSocketServer(); 49 | server.start({ 50 | // close server when running 51 | ready: () => server.close(), 52 | connection: noop, 53 | close: () => t.throws( 54 | // start server when already shut down 55 | () => server.start({ 56 | ready: noop, 57 | connection: noop, 58 | close: noop, 59 | }), 60 | ), 61 | }); 62 | }); 63 | 64 | test('workerSocketServer.close() - when RUNNING', (t) => { 65 | t.plan(2); 66 | 67 | const server = createWorkerSocketServer(); 68 | server.start({ 69 | // close server when running 70 | ready: () => server.close(), 71 | connection: noop, 72 | close: (error) => { 73 | t.equal(error, undefined, 'should invoke close handler with no error message'); 74 | t.equal(server.state(), MuSocketServerState.SHUTDOWN, 'should change state to MuSocketServerState.SHUTDOWN'); 75 | }, 76 | }); 77 | }); 78 | 79 | test('workerSocketServer.close() - when INIT', (t) => { 80 | const server = createWorkerSocketServer(); 81 | // close server when init 82 | server.close(); 83 | t.equal(server.state(), MuSocketServerState.SHUTDOWN, 'should change state to MuSocketServerState.SHUTDOWN'); 84 | t.end(); 85 | }); 86 | 87 | test('workerSocketServer.close() - when SHUTDOWN', (t) => { 88 | const server = createWorkerSocketServer(); 89 | server.start({ 90 | // close server when running 91 | ready: () => server.close(), 92 | connection: noop, 93 | close: () => { 94 | // close server when already shut down 95 | server.close(); 96 | 97 | t.ok(true, 'should not invoke close handler'); 98 | t.end(); 99 | }, 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/util/error.ts: -------------------------------------------------------------------------------- 1 | export function makeError (path:string) { 2 | return function (errOrMsg:Error|string) { 3 | const msg = typeof errOrMsg === 'string' ? errOrMsg : errOrMsg.toString(); 4 | return new Error(`${msg} [mudb/${path}]`); 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /src/util/parse-body.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as zlib from 'zlib'; 3 | 4 | function decodeStream (req:http.IncomingMessage) { 5 | const encoding = req.headers['content-encoding']; 6 | if (encoding === 'deflate') { 7 | const stream = zlib.createInflate(); 8 | req.pipe(stream); 9 | return stream; 10 | } else if (encoding === 'gzip') { 11 | const stream = zlib.createGunzip(); 12 | req.pipe(stream); 13 | return stream; 14 | } else if (!encoding || encoding === 'identity') { 15 | return req; 16 | } 17 | throw new Error(`unknown encoding: ${encoding}`); 18 | } 19 | 20 | export function getRawBody (req:http.IncomingMessage, length:number) : Promise { 21 | return new Promise(function executor (resolve, reject) { 22 | if (length === 0) { 23 | return resolve(Buffer.alloc(0)); 24 | } 25 | 26 | let complete = false; 27 | let received = 0; 28 | const buffers:Buffer[] = []; 29 | const stream = decodeStream(req); 30 | 31 | stream.on('aborted', onAborted); 32 | stream.on('close', cleanup); 33 | stream.on('data', onData); 34 | stream.on('end', onEnd); 35 | stream.on('error', onEnd); 36 | 37 | function cleanup () { 38 | buffers.length = 0; 39 | stream.removeListener('aborted', onAborted); 40 | stream.removeListener('data', onData); 41 | stream.removeListener('end', onEnd); 42 | stream.removeListener('error', onEnd); 43 | stream.removeListener('close', cleanup); 44 | } 45 | 46 | function done (error:string|undefined, buf?:Buffer) { 47 | complete = true; 48 | setImmediate(() => { 49 | cleanup(); 50 | if (error) { 51 | req.unpipe(); 52 | stream.pause(); 53 | reject(error); 54 | } else { 55 | if (buf) { 56 | resolve(buf); 57 | } else { 58 | reject(new Error('invalid buffer')); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | function onAborted () { 65 | if (!complete) { 66 | done('request aborted'); 67 | } 68 | } 69 | 70 | function onData (chunk:Buffer) { 71 | if (complete) { 72 | return; 73 | } 74 | received += chunk.length; 75 | if (received > length) { 76 | done('request entity too large'); 77 | } else { 78 | buffers.push(chunk); 79 | } 80 | } 81 | 82 | function onEnd (err) { 83 | if (complete) { 84 | return; 85 | } else if (err) { 86 | return done(err); 87 | } else if (received !== length) { 88 | done('request size did not match content length'); 89 | } else { 90 | done(void 0, Buffer.concat(buffers)); 91 | } 92 | } 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/util/port.ts: -------------------------------------------------------------------------------- 1 | import * as tcp from 'net'; 2 | 3 | export function findPort (cb:(port:number) => void) { 4 | const server = tcp.createServer(); 5 | server.on('error', (e) => console.error(e)); 6 | server.unref(); 7 | server.listen(() => { 8 | const addr = server.address(); 9 | if (typeof addr === 'string') { 10 | server.close(() => cb(+addr)); 11 | } else { 12 | const port = addr.port; 13 | server.close(() => cb(port)); 14 | } 15 | }); 16 | } 17 | 18 | export function findPortAsync () : Promise { 19 | return new Promise((resolve) => { 20 | findPort(resolve); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/util/random.ts: -------------------------------------------------------------------------------- 1 | // boolean 2 | 3 | export function randBool () { 4 | return Math.random() >= 0.5; 5 | } 6 | 7 | // integer 8 | 9 | export function randInt (min:number, max:number) : number { 10 | return Math.random() * (max - min + 1) + min | 0; 11 | } 12 | 13 | export function randInt8 () { 14 | return randInt(-0x80, 0x7F); 15 | } 16 | 17 | export function randInt16 () { 18 | return randInt(-0x8000, 0x7FFF); 19 | } 20 | 21 | export function randInt32 () { 22 | return randInt(-0x80000000, 0x7FFFFFFF); 23 | } 24 | 25 | export function randUint8 () { 26 | return randInt(0, 0xFF); 27 | } 28 | 29 | export function randUint16 () { 30 | return randInt(0, 0xFFFF); 31 | } 32 | 33 | export function randUint32 () { 34 | return randInt(0, 0xFFFFFFFF) >>> 0; 35 | } 36 | 37 | // float 38 | 39 | const dv = new DataView(new ArrayBuffer(8)); 40 | 41 | export function randFloat32 () { 42 | let f; 43 | do { 44 | dv.setUint32(0, randUint32(), true); 45 | f = dv.getFloat32(0, true); 46 | } while (isNaN(f)); 47 | return f; 48 | } 49 | 50 | export function randFloat64 () { 51 | let f; 52 | do { 53 | dv.setUint32(0, randUint32(), true); 54 | dv.setUint32(4, randUint32(), true); 55 | f = dv.getFloat64(0, true); 56 | } while (isNaN(f)); 57 | return f; 58 | } 59 | -------------------------------------------------------------------------------- /src/util/stringify.ts: -------------------------------------------------------------------------------- 1 | export function stableStringify (base:any) : string|void { 2 | const result:string[] = []; 3 | const seen:object[] = []; 4 | function stringify (x_:any) : boolean { 5 | const x = x_ && x_.toJSON && typeof x_.toJSON === 'function' ? x_.toJSON() : x_; 6 | if (x === undefined) { 7 | return false; 8 | } 9 | // handle base cases 10 | if (x === true) { 11 | result.push('true'); 12 | return true; 13 | } 14 | if (x === false) { 15 | result.push('false'); 16 | return true; 17 | } 18 | if (typeof x === 'number') { 19 | result.push(isFinite(x) ? '' + x : 'null'); 20 | return true; 21 | } 22 | if (typeof x !== 'object') { 23 | const res = JSON.stringify(x); 24 | if (typeof res === 'undefined') { 25 | return false; 26 | } 27 | result.push(res); 28 | return true; 29 | } 30 | if (x === null) { 31 | result.push('null'); 32 | return true; 33 | } 34 | 35 | // circular reference check 36 | if (seen.indexOf(x) >= 0) { 37 | throw new TypeError('Converting circular structure to JSON'); 38 | } 39 | seen.push(x); 40 | 41 | if (Array.isArray(x)) { 42 | result.push('['); 43 | for (let i = 0; i < x.length; ++i) { 44 | if (!stringify(x[i])) { 45 | result.push('null'); 46 | } 47 | if (i < x.length - 1) { 48 | result.push(','); 49 | } 50 | } 51 | result.push(']'); 52 | } else { 53 | result.push('{'); 54 | const keys = Object.keys(x).sort(); 55 | let needsComma = false; 56 | for (let i = 0; i < keys.length; ++i) { 57 | const key = keys[i]; 58 | if (needsComma) { 59 | result.push(','); 60 | needsComma = false; 61 | } 62 | result.push(`${JSON.stringify(key)}:`); 63 | if (!stringify(x[key])) { 64 | result.pop(); 65 | } else { 66 | needsComma = true; 67 | } 68 | } 69 | result.push('}'); 70 | } 71 | 72 | // clear circular check 73 | seen[seen.indexOf(x)] = seen[seen.length - 1]; 74 | seen.pop(); 75 | 76 | return true; 77 | } 78 | if (!stringify(base)) { 79 | return void 0; 80 | } 81 | return result.join(''); 82 | } 83 | -------------------------------------------------------------------------------- /src/util/test/error.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { makeError } from '../error'; 3 | 4 | test('makeError()(string)', (t) => { 5 | const path = 'util/test/error'; 6 | const msg = 'contrived error'; 7 | const error = makeError(path); 8 | 9 | try { 10 | throw error(msg); 11 | } catch (e) { 12 | t.equal(e.toString(), `Error: ${msg} [mudb/${path}]`, e.toString()); 13 | t.end(); 14 | } 15 | }); 16 | 17 | test('makeError()(Error)', (t) => { 18 | const path = 'util/test/error'; 19 | const msg = 'contrived error'; 20 | const error = makeError(path); 21 | 22 | try { 23 | throw error(new Error(msg)); 24 | } catch (e) { 25 | t.equal(e.toString(), `Error: Error: ${msg} [mudb/${path}]`, e.toString()); 26 | t.end(); 27 | } 28 | }); 29 | 30 | test('makeError()(SyntaxError)', (t) => { 31 | const path = 'util/test/error'; 32 | const error = makeError(path); 33 | 34 | try { 35 | JSON.parse(''); 36 | } catch (e) { 37 | t.true(/^Error: SyntaxError: /.test(error(e).toString()), error(e).toString()); 38 | t.end(); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/util/test/stringify.ts: -------------------------------------------------------------------------------- 1 | import * as test from 'tape'; 2 | import { stableStringify } from '../stringify'; 3 | 4 | test('toJSON()', (t) => { 5 | const now = new Date(); 6 | t.equal(stableStringify(now), `"${now.toJSON()}"`); 7 | t.equal(stableStringify({d: now}), `{"d":"${now.toJSON()}"}`); 8 | t.end(); 9 | }); 10 | 11 | test('undefined & null', (t) => { 12 | t.equal(stableStringify({a: 0, b: undefined, c: 1}), '{"a":0,"c":1}'); 13 | t.equal(stableStringify({a: 0, b: null, c: 1}), '{"a":0,"b":null,"c":1}'); 14 | t.equal(stableStringify([0, undefined, 1]), '[0,null,1]'); 15 | t.equal(stableStringify([0, null, 1]), '[0,null,1]'); 16 | t.end(); 17 | }); 18 | 19 | test('NaN & Infinity', (t) => { 20 | t.equal(stableStringify({a: 0, b: NaN, c: 1}), '{"a":0,"b":null,"c":1}'); 21 | t.equal(stableStringify({a: 0, b: Infinity, c: 1}), '{"a":0,"b":null,"c":1}'); 22 | t.equal(stableStringify([0, NaN, 1]), '[0,null,1]'); 23 | t.equal(stableStringify([0, Infinity, 1]), '[0,null,1]'); 24 | t.end(); 25 | }); 26 | 27 | test('sorting', (t) => { 28 | t.equal(stableStringify({}), '{}'); 29 | t.equal(stableStringify([]), '[]'); 30 | const flat = {b: false, ascii: 'mudb', u: 'Iñtërnâtiônàlizætiøn☃💩', f32: 0.5, n: null}; 31 | t.equal(stableStringify(flat), `{"ascii":${JSON.stringify('mudb')},"b":false,"f32":0.5,"n":null,"u":"Iñtërnâtiônàlizætiøn☃💩"}`); 32 | const nested = {e: {}, a: [], o: {z: {b: {a: 0, c: 0, b: 0}, a: 0, c: 0}, a: [{z: 0, a: [{c: 0, b: 0, a: 0}]}]}, f64: -1.5e308, b: true}; 33 | t.equal(stableStringify(nested), '{"a":[],"b":true,"e":{},"f64":-1.5e+308,"o":{"a":[{"a":[{"a":0,"b":0,"c":0}],"z":0}],"z":{"a":0,"b":{"a":0,"b":0,"c":0},"c":0}}}'); 34 | t.end(); 35 | }); 36 | 37 | test('circular references', (t) => { 38 | const o = {a: 0, z: 1}; 39 | o['o'] = o; 40 | t.throws(() => stableStringify(o), TypeError, 'Converting circular structure to JSON'); 41 | t.end(); 42 | }); 43 | 44 | test('repeated references', (t) => { 45 | const o = {a: 0}; 46 | const p = {b: o, c: o}; 47 | t.equal(stableStringify(p), '{"b":{"a":0},"c":{"a":0}}'); 48 | t.end(); 49 | }); 50 | -------------------------------------------------------------------------------- /tool/mudo/.npmignore: -------------------------------------------------------------------------------- 1 | # view config 2 | .DS_Store 3 | Desktop.ini 4 | 5 | # logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | *npm-debug.log 12 | 13 | # deps 14 | node_modules 15 | 16 | # build 17 | build 18 | 19 | # misc 20 | .jsbeautifyrc 21 | yarn.lock 22 | -------------------------------------------------------------------------------- /tool/mudo/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mikola Lysenko, Shenzhen DianMao Digital Technology Co., Ltd. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /tool/mudo/README.md: -------------------------------------------------------------------------------- 1 | # mudo 2 | Local development server for mudb. Makes it easy to start mudb client/server pairs using some opinionated conventions. 3 | 4 | # install 5 | 6 | ``` 7 | npm i mudo 8 | ``` 9 | 10 | # example usage 11 | To run `mudo`, execute the following command: 12 | 13 | ```sh 14 | mudo --client myclient.js --server myserver.js --open 15 | ``` 16 | 17 | Where `myclient.js` and `myserver.js` are implemented as follows: 18 | 19 | **myclient.js** 20 | 21 | ```javascript 22 | module.exports = function (muClient) { 23 | // client implementation ... 24 | 25 | muClient.start(); 26 | } 27 | ``` 28 | 29 | **myserver.js** 30 | ```javascript 31 | module.exports = function (muServer) { 32 | // server implementation ... 33 | 34 | muServer.start(); 35 | } 36 | ``` 37 | 38 | By default `mudo` uses `mulocal-socket` to connect the client and server instances locally. Multiplayer games over `muweb-socket` can be run by passing the `--socket websocket` option to mudo. 39 | 40 | # cli 41 | 42 | ## options 43 | 44 | ### `client` 45 | Path to the client script. Defaults to `client.js` 46 | 47 | ### `server` 48 | Path to the server script. Defaults to `server.js` 49 | 50 | ### `socket` 51 | Socket type to use. Possible options: 52 | 53 | * `local`: uses `mulocal-socket` (default) 54 | * `websocket`: uses `muweb-socket` 55 | 56 | ### `open` 57 | Opens a browser window with the socket server contents listed 58 | 59 | # api 60 | 61 | **TODO** 62 | 63 | # credits 64 | Copyright (c) 2017 Mikola Lysenko, Shenzhen Dianmao Technology Company Limited 65 | 66 | 67 | -------------------------------------------------------------------------------- /tool/mudo/bin/mudo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | var mudo_1 = require("../mudo"); 5 | var minimist = require("minimist"); 6 | var argv = minimist(process.argv.slice(2)); 7 | var mudoSpec = { 8 | client: argv.client || 'client.js', 9 | server: argv.server || 'server.js', 10 | }; 11 | if ('socket' in argv) { 12 | var socketType = argv.socket.toUpperCase(); 13 | if (!(socketType in mudo_1.MUDO_SOCKET_TYPES)) { 14 | throw new Error("unknown socket type. must be one of " + Object.keys(mudo_1.MUDO_SOCKET_TYPES).join()); 15 | } 16 | mudoSpec.socket = mudo_1.MUDO_SOCKET_TYPES[socketType]; 17 | } 18 | if ('port' in argv) { 19 | mudoSpec.port = argv.port | 0; 20 | } 21 | if ('host' in argv) { 22 | mudoSpec.host = argv.host; 23 | } 24 | if ('cors' in argv) { 25 | mudoSpec.cors = !!argv.cors; 26 | } 27 | if ('ssl' in argv) { 28 | mudoSpec.ssl = !!argv.ssl; 29 | } 30 | if ('cert' in argv) { 31 | mudoSpec.cert = argv.cert; 32 | } 33 | if ('open' in argv) { 34 | mudoSpec.open = argv.open; 35 | } 36 | if ('bundle' in argv) { 37 | mudoSpec.serve = argv.bundle; 38 | } 39 | mudo_1.createMudo(mudoSpec); 40 | //# sourceMappingURL=mudo.js.map -------------------------------------------------------------------------------- /tool/mudo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mudo", 3 | "version": "0.1.8", 4 | "description": "Development framework for prototyping mudb applications", 5 | "main": "mudo.js", 6 | "bin": { 7 | "mudo": "./bin/mudo.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mikolalysenko/mudb.git" 12 | }, 13 | "keywords": [ 14 | "mudb", 15 | "prototyping", 16 | "framework" 17 | ], 18 | "author": "Mikola Lysenko", 19 | "license": "MIT", 20 | "dependencies": { 21 | "browserify": "^16.2.3", 22 | "budo": "^11.6.0", 23 | "minimist": "^1.2.0", 24 | "mudb": "^0.13.0", 25 | "temp": "^0.8.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tool/mudo/src/bin/mudo.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { createMudo, MUDO_SOCKET_TYPES, MudoSpec } from '../mudo'; 3 | import minimist = require('minimist'); 4 | 5 | const argv = minimist(process.argv.slice(2)); 6 | 7 | const mudoSpec:MudoSpec = { 8 | client: argv.client || 'client.js', 9 | server: argv.server || 'server.js', 10 | }; 11 | 12 | if ('socket' in argv) { 13 | const socketType = (argv.socket).toUpperCase(); 14 | if (!(socketType in MUDO_SOCKET_TYPES)) { 15 | throw new Error(`unknown socket type. must be one of ${Object.keys(MUDO_SOCKET_TYPES).join()}`); 16 | } 17 | mudoSpec.socket = MUDO_SOCKET_TYPES[socketType]; 18 | } 19 | 20 | if ('port' in argv) { 21 | mudoSpec.port = argv.port | 0; 22 | } 23 | if ('host' in argv) { 24 | mudoSpec.host = argv.host; 25 | } 26 | if ('cors' in argv) { 27 | mudoSpec.cors = !!argv.cors; 28 | } 29 | if ('ssl' in argv) { 30 | mudoSpec.ssl = !!argv.ssl; 31 | } 32 | if ('cert' in argv) { 33 | mudoSpec.cert = argv.cert; 34 | } 35 | if ('open' in argv) { 36 | mudoSpec.open = argv.open; 37 | } 38 | if ('bundle' in argv) { 39 | mudoSpec.serve = argv.bundle; 40 | } 41 | 42 | createMudo(mudoSpec); 43 | -------------------------------------------------------------------------------- /tool/mudo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["dom", "es5"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noImplicitReturns": true, 10 | "noImplicitAny": false, 11 | "outDir": ".", 12 | "plugins": [ 13 | { 14 | "name": "tslint-language-service" 15 | } 16 | ], 17 | "removeComments": true, 18 | "rootDir": "src", 19 | "sourceMap": true, 20 | "strict": true, 21 | "target": "es5" 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "build", 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tool/mudo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "arrow-return-shorthand": true, 5 | "array-type": [true, "array"], 6 | "await-promise": true, 7 | "curly": true, 8 | "no-arg": true, 9 | "no-conditional-assignment": true, 10 | "no-construct": true, 11 | "no-debugger": true, 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-floating-promises": true, 15 | "no-for-in-array": true, 16 | "no-misused-new": true, 17 | "no-null-keyword": false, 18 | "no-shadowed-variable": true, 19 | "no-unsafe-finally": true, 20 | "no-unused-variable": true, 21 | "no-use-before-declare": true, 22 | "indent": [true, "spaces"], 23 | "linebreak-style": [true, "LF"], 24 | "label-position": true, 25 | "no-default-export": false, 26 | "no-string-throw": true, 27 | "no-trailing-whitespace": true, 28 | "prefer-const": true, 29 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 30 | "typedef": [ 31 | true, 32 | "property-declaration" 33 | ], 34 | "typedef-whitespace": 35 | [ 36 | true, 37 | { 38 | "call-signature": "space", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | }, 44 | { 45 | "call-signature": "space", 46 | "index-signature": "nospace", 47 | "parameter": "nospace", 48 | "property-declaration": "nospace", 49 | "variable-declaration": "nospace" 50 | } 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-module", 56 | "check-operator", 57 | "check-separator" 58 | ], 59 | "align": [true, "arguments", "parameters", "statements"], 60 | "class-name": true, 61 | "member-access": true, 62 | "new-parens": true, 63 | "no-consecutive-blank-lines": true, 64 | "no-reference": true, 65 | "one-variable-per-declaration": [true, "ignore-for-loop"], 66 | "one-line": [ 67 | true, 68 | "check-catch", 69 | "check-finally", 70 | "check-else", 71 | "check-open-brace", 72 | "check-whitespace" 73 | ], 74 | "promise-function-async": true, 75 | "quotemark": [true, "single", "avoid-escape"], 76 | "semicolon": [true, "always"], 77 | "variable-name": [true, "ban-keywords"] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "lib": ["dom", "es5", "ES2015", "es6", "es2017"], 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "noImplicitAny": false, 10 | "noImplicitReturns": true, 11 | "outDir": ".", 12 | "plugins": [ 13 | { 14 | "name": "tslint-language-service" 15 | } 16 | ], 17 | "removeComments": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "strictPropertyInitialization": false, 21 | "target": "es2017" 22 | }, 23 | "include": [ 24 | "src/**/*", 25 | ], 26 | "exclude": [ 27 | "build", 28 | "node_modules" 29 | ], 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "arrow-return-shorthand": true, 5 | "array-type": [true, "array"], 6 | "await-promise": true, 7 | "curly": true, 8 | "no-arg": true, 9 | "no-conditional-assignment": true, 10 | "no-construct": true, 11 | "no-debugger": true, 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-floating-promises": true, 15 | "no-for-in-array": true, 16 | "no-misused-new": true, 17 | "no-null-keyword": false, 18 | "no-shadowed-variable": true, 19 | "no-unsafe-finally": true, 20 | "no-unused-variable": true, 21 | "no-use-before-declare": true, 22 | "indent": [true, "spaces"], 23 | "linebreak-style": [true, "LF"], 24 | "label-position": true, 25 | "no-default-export": false, 26 | "no-string-throw": true, 27 | "no-trailing-whitespace": true, 28 | "prefer-const": true, 29 | "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], 30 | "typedef": [ 31 | true, 32 | "property-declaration" 33 | ], 34 | "typedef-whitespace": 35 | [ 36 | true, 37 | { 38 | "call-signature": "space", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | }, 44 | { 45 | "call-signature": "space", 46 | "index-signature": "nospace", 47 | "parameter": "nospace", 48 | "property-declaration": "nospace", 49 | "variable-declaration": "nospace" 50 | } 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-module", 56 | "check-operator", 57 | "check-separator" 58 | ], 59 | "align": [true, "parameters", "statements"], 60 | "class-name": true, 61 | "member-access": true, 62 | "new-parens": true, 63 | "no-consecutive-blank-lines": true, 64 | "no-reference": true, 65 | "one-variable-per-declaration": [true, "ignore-for-loop"], 66 | "one-line": [ 67 | true, 68 | "check-catch", 69 | "check-finally", 70 | "check-else", 71 | "check-open-brace", 72 | "check-whitespace" 73 | ], 74 | "quotemark": [true, "single", "avoid-escape"], 75 | "semicolon": [true, "always"], 76 | "variable-name": [true, "ban-keywords"] 77 | } 78 | } 79 | --------------------------------------------------------------------------------