├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── config.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── bundle ├── typed-array-buffer-schema.js └── typed-array-buffer-schema.js.LICENSE.txt ├── package.json ├── readme ├── logo.png └── logo.svg ├── src ├── bundle.ts ├── deep-sort-object.ts ├── dev.ts ├── index.ts ├── lib.ts ├── model.ts ├── schema.ts ├── serialize.ts └── views.ts ├── test ├── benchmark.test.js ├── dataViews.test.js ├── emptyData.test.js ├── schemaId.test.js ├── serializeDeserialize.test.js ├── simple.test.js └── specialTypes.test.js ├── tsconfig.json ├── webpack.bundle.js └── webpack.bundle.tmp.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: yandeu 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Have a question?** 13 | Join the [discussions](https://github.com/geckosio/geckos.io/discussions) instead. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/geckosio/geckos.io/discussions 5 | about: Ask questions and discuss with other community members 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [18.x, 16.x] 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | 22 | - name: Install and Build 23 | run: | 24 | npm install 25 | npm run build 26 | 27 | - name: Test 28 | run: npm test 29 | 30 | - name: Webpack Bundle 31 | run: npm run bundle:tmp 32 | 33 | - name: Upload code coverage 34 | uses: codecov/codecov-action@v2 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /lib 3 | /node_modules 4 | /package-lock.json 5 | /bundle/typed-array-buffer-schema.tmp.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | !/.npmrc 4 | !/README.md 5 | !/bundle 6 | !/lib 7 | !/package.json 8 | !/readme -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | "@yandeu/prettier-config" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Yannick Deubel (https://github.com/yandeu); Project Url: https://github.com/geckosio/typed-array-buffer-schema 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | logo 4 | 5 | # Typed Array Buffer Schema 6 | 7 | ## A Schema based Object to Buffer converter 8 | 9 | #### Easily convert/compress your JavaScript Objects to Binary Data using a simple to use Schema. 10 | 11 | [![NPM version](https://img.shields.io/npm/v/@geckos.io/typed-array-buffer-schema.svg?style=flat-square)](https://www.npmjs.com/package/@geckos.io/typed-array-buffer-schema) 12 | [![Github Workflow](https://img.shields.io/github/workflow/status/geckosio/typed-array-buffer-schema/CI/master?label=github%20build&logo=github&style=flat-square)](https://github.com/geckosio/typed-array-buffer-schema/actions?query=workflow%3ACI) 13 | [![GitHub last commit](https://img.shields.io/github/last-commit/geckosio/typed-array-buffer-schema?style=flat-square)](https://github.com/geckosio/typed-array-buffer-schema/commits/master) 14 | [![Downloads](https://img.shields.io/npm/dm/@geckos.io/typed-array-buffer-schema.svg?style=flat-square)](https://www.npmjs.com/package/@geckos.io/typed-array-buffer-schema) 15 | [![Codecov](https://img.shields.io/codecov/c/github/geckosio/typed-array-buffer-schema?logo=codecov&style=flat-square)](https://codecov.io/gh/geckosio/typed-array-buffer-schema) 16 | [![build with TypeScript](https://img.shields.io/badge/built%20with-TypeScript-blue?style=flat-square)](https://www.typescriptlang.org/) 17 | 18 |
19 | 20 | --- 21 | 22 | ## Introduction 23 | 24 | Checkout this short introduction video on YouTube! 25 | 26 | https://youtu.be/TBd1miOrLPQ 27 | 28 | ## Install 29 | 30 | Install from npm. 31 | 32 | ```console 33 | npm install @geckos.io/typed-array-buffer-schema 34 | ``` 35 | 36 | Or use the bundled version. 37 | 38 | ```html 39 | 40 | 44 | ``` 45 | 46 | ## Snapshot Interpolation 47 | 48 | You can easily combine this library with the Snapshot Interpolation library [@geckos.io/snapshot-interpolation](https://www.npmjs.com/package/@geckos.io/snapshot-interpolation). 49 | 50 | ## Usage 51 | 52 | #### model.js 53 | 54 | ```js 55 | import { BufferSchema, Model } from '@geckos.io/typed-array-buffer-schema' 56 | import { uint8, int16, uint16, int64, string8 } from '@geckos.io/typed-array-buffer-schema' 57 | 58 | const playerSchema = BufferSchema.schema('player', { 59 | id: uint8, 60 | // The length parameter controls the length of a String8 or String16 61 | name: { type: string8, length: 6 }, 62 | // The digits parameter controls where the decimal point is placed 63 | // Therefore, it divides the maximum and minimum values by 10^n 64 | x: { type: int16, digits: 2 }, 65 | y: { type: int16, digits: 2 } 66 | }) 67 | 68 | const towerSchema = BufferSchema.schema('tower', { 69 | id: uint8, 70 | health: uint8, 71 | team: uint8 72 | }) 73 | 74 | const mainSchema = BufferSchema.schema('main', { 75 | time: int64, 76 | tick: uint16, 77 | players: [playerSchema], 78 | towers: [towerSchema] 79 | }) 80 | 81 | export const mainModel = new Model(mainSchema) 82 | 83 | // if you get the error "RangeError: Offset is outside the bounds of the DataView", increase the max. bufferSize. 84 | // default is 8 (8KB). 85 | export const mainModel = new Model(mainSchema, 8) 86 | ``` 87 | 88 | #### server.js 89 | 90 | ```js 91 | import { mainModel } from './model' 92 | 93 | const gameState = { 94 | time: new Date().getTime(), 95 | tick: 32580, 96 | players: [ 97 | { id: 0, name: 'Mistin', x: -14.43, y: 47.78 }, 98 | { id: 1, name: 'Coobim', x: 21.85, y: -78.48 } 99 | ], 100 | towers: [ 101 | { id: 0, health: 100, team: 0 }, 102 | { id: 1, health: 89, team: 0 }, 103 | { id: 2, health: 45, team: 1 } 104 | ] 105 | } 106 | 107 | const buffer = mainModel.toBuffer(gameState) 108 | 109 | // toBuffer() shrunk the byte size from 241 to only 56 110 | // that is -77% compression! 111 | console.log(JSON.stringify(gameState).length) // 241 112 | console.log(buffer.byteLength) // 56 113 | 114 | // send the buffer to the client (using geckos.io or any other library) 115 | sendMessage(buffer) 116 | ``` 117 | 118 | #### client.js 119 | 120 | ```js 121 | import { mainModel } from './model' 122 | 123 | onMessage(buffer => { 124 | // access your game state 125 | const gameState = mainModel.fromBuffer(buffer) 126 | }) 127 | ``` 128 | 129 | ## Schema ID 130 | 131 | Each Schema has an unique ID. To get the Schema ID from the Schema, Model or Buffer, use the helper functions listed below: 132 | 133 | ```ts 134 | // get the schema id 135 | const schemaId = BufferSchema.getIdFromSchema(schema) 136 | 137 | // get the id of the top level schema (added via new Schema()) 138 | const modelId = BufferSchema.getIdFromModel(model) 139 | 140 | // get the id of the top level schema 141 | const bufferId = BufferSchema.getIdFromBuffer(buffer) 142 | ``` 143 | 144 | ## DataViews 145 | 146 | A list of all supported dataViews 147 | 148 | ```js 149 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays 150 | 151 | /** -128 to 127 (1 byte) */ 152 | export const int8 = { _type: 'Int8Array', _bytes: 1 } 153 | /** 0 to 255 (1 byte) */ 154 | export const uint8 = { _type: 'Uint8Array', _bytes: 1 } 155 | 156 | /** -32768 to 32767 (2 bytes) */ 157 | export const int16 = { _type: 'Int16Array', _bytes: 2 } 158 | /** 0 to 65535 (2 bytes) */ 159 | export const uint16 = { _type: 'Uint16Array', _bytes: 2 } 160 | 161 | /** -2147483648 to 2147483647 (4 bytes) */ 162 | export const int32 = { _type: 'Int32Array', _bytes: 4 } 163 | /** 0 to 4294967295 (4 bytes) */ 164 | export const uint32 = { _type: 'Uint32Array', _bytes: 4 } 165 | 166 | /** -2^63 to 2^63-1 (8 bytes) */ 167 | export const int64 = { _type: 'BigInt64Array', _bytes: 8 } 168 | /** 0 to 2^64-1 (8 bytes) */ 169 | export const uint64 = { _type: 'BigUint64Array', _bytes: 8 } 170 | 171 | /** 1.2×10-38 to 3.4×1038 (7 significant digits e.g., 1.123456) (4 bytes) */ 172 | export const float32 = { _type: 'Float32Array', _bytes: 4 } 173 | 174 | /** 5.0×10-324 to 1.8×10308 (16 significant digits e.g., 1.123...15) (8 bytes) */ 175 | export const float64 = { _type: 'Float64Array', _bytes: 8 } 176 | 177 | /** 1 byte per character */ 178 | export const string8 = { _type: 'String8', _bytes: 1 } 179 | /** 2 bytes per character */ 180 | export const string16 = { _type: 'String16', _bytes: 2 } 181 | 182 | /** An array of 7 booleans */ 183 | export const bool8 = { _type: 'BitArray8', _bytes: 1 } 184 | /** An array of 15 booleans */ 185 | export const bool16 = { _type: 'BitArray16', _bytes: 2 } 186 | ``` 187 | -------------------------------------------------------------------------------- /bundle/typed-array-buffer-schema.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see typed-array-buffer-schema.js.LICENSE.txt */ 2 | var Schema;(()=>{var t={6057:(t,e)=>{"use strict";function r(t){return"[object Object]"===Object.prototype.toString.call(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.isPlainObject=function(t){var e,i;return!1!==r(t)&&(void 0===(e=t.constructor)||!1!==r(i=e.prototype)&&!1!==i.hasOwnProperty("isPrototypeOf"))}},1989:(t,e,r)=>{var i=r(1789),n=r(401),o=r(7667),s=r(1327),a=r(1866);function l(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e{var i=r(7040),n=r(4125),o=r(2117),s=r(7518),a=r(4705);function l(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e{var i=r(852)(r(5639),"Map");t.exports=i},3369:(t,e,r)=>{var i=r(4785),n=r(1285),o=r(6e3),s=r(9916),a=r(5265);function l(t){var e=-1,r=null==t?0:t.length;for(this.clear();++e{var i=r(5639).Symbol;t.exports=i},9932:t=>{t.exports=function(t,e){for(var r=-1,i=null==t?0:t.length,n=Array(i);++r{var i=r(9465),n=r(7813),o=Object.prototype.hasOwnProperty;t.exports=function(t,e,r){var s=t[e];o.call(t,e)&&n(s,r)&&(void 0!==r||e in t)||i(t,e,r)}},8470:(t,e,r)=>{var i=r(7813);t.exports=function(t,e){for(var r=t.length;r--;)if(i(t[r][0],e))return r;return-1}},9465:(t,e,r)=>{var i=r(8777);t.exports=function(t,e,r){"__proto__"==e&&i?i(t,e,{configurable:!0,enumerable:!0,value:r,writable:!0}):t[e]=r}},4239:(t,e,r)=>{var i=r(2705),n=r(9607),o=r(2333),s=i?i.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":s&&s in Object(t)?n(t):o(t)}},8458:(t,e,r)=>{var i=r(3560),n=r(5346),o=r(3218),s=r(346),a=/^\[object .+?Constructor\]$/,l=Function.prototype,u=Object.prototype,c=l.toString,p=u.hasOwnProperty,y=RegExp("^"+c.call(p).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=function(t){return!(!o(t)||n(t))&&(i(t)?y:a).test(s(t))}},611:(t,e,r)=>{var i=r(4865),n=r(1811),o=r(5776),s=r(3218),a=r(327);t.exports=function(t,e,r,l){if(!s(t))return t;for(var u=-1,c=(e=n(e,t)).length,p=c-1,y=t;null!=y&&++u{var i=r(2705),n=r(9932),o=r(1469),s=r(3448),a=i?i.prototype:void 0,l=a?a.toString:void 0;t.exports=function t(e){if("string"==typeof e)return e;if(o(e))return n(e,t)+"";if(s(e))return l?l.call(e):"";var r=e+"";return"0"==r&&1/e==-1/0?"-0":r}},1811:(t,e,r)=>{var i=r(1469),n=r(5403),o=r(5514),s=r(9833);t.exports=function(t,e){return i(t)?t:n(t,e)?[t]:o(s(t))}},4429:(t,e,r)=>{var i=r(5639)["__core-js_shared__"];t.exports=i},8777:(t,e,r)=>{var i=r(852),n=function(){try{var t=i(Object,"defineProperty");return t({},"",{}),t}catch(t){}}();t.exports=n},1957:(t,e,r)=>{var i="object"==typeof r.g&&r.g&&r.g.Object===Object&&r.g;t.exports=i},5050:(t,e,r)=>{var i=r(7019);t.exports=function(t,e){var r=t.__data__;return i(e)?r["string"==typeof e?"string":"hash"]:r.map}},852:(t,e,r)=>{var i=r(8458),n=r(7801);t.exports=function(t,e){var r=n(t,e);return i(r)?r:void 0}},9607:(t,e,r)=>{var i=r(2705),n=Object.prototype,o=n.hasOwnProperty,s=n.toString,a=i?i.toStringTag:void 0;t.exports=function(t){var e=o.call(t,a),r=t[a];try{t[a]=void 0;var i=!0}catch(t){}var n=s.call(t);return i&&(e?t[a]=r:delete t[a]),n}},7801:t=>{t.exports=function(t,e){return null==t?void 0:t[e]}},1789:(t,e,r)=>{var i=r(4536);t.exports=function(){this.__data__=i?i(null):{},this.size=0}},401:t=>{t.exports=function(t){var e=this.has(t)&&delete this.__data__[t];return this.size-=e?1:0,e}},7667:(t,e,r)=>{var i=r(4536),n=Object.prototype.hasOwnProperty;t.exports=function(t){var e=this.__data__;if(i){var r=e[t];return"__lodash_hash_undefined__"===r?void 0:r}return n.call(e,t)?e[t]:void 0}},1327:(t,e,r)=>{var i=r(4536),n=Object.prototype.hasOwnProperty;t.exports=function(t){var e=this.__data__;return i?void 0!==e[t]:n.call(e,t)}},1866:(t,e,r)=>{var i=r(4536);t.exports=function(t,e){var r=this.__data__;return this.size+=this.has(t)?0:1,r[t]=i&&void 0===e?"__lodash_hash_undefined__":e,this}},5776:t=>{var e=/^(?:0|[1-9]\d*)$/;t.exports=function(t,r){var i=typeof t;return!!(r=null==r?9007199254740991:r)&&("number"==i||"symbol"!=i&&e.test(t))&&t>-1&&t%1==0&&t{var i=r(1469),n=r(3448),o=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,s=/^\w*$/;t.exports=function(t,e){if(i(t))return!1;var r=typeof t;return!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=t&&!n(t))||s.test(t)||!o.test(t)||null!=e&&t in Object(e)}},7019:t=>{t.exports=function(t){var e=typeof t;return"string"==e||"number"==e||"symbol"==e||"boolean"==e?"__proto__"!==t:null===t}},5346:(t,e,r)=>{var i,n=r(4429),o=(i=/[^.]+$/.exec(n&&n.keys&&n.keys.IE_PROTO||""))?"Symbol(src)_1."+i:"";t.exports=function(t){return!!o&&o in t}},7040:t=>{t.exports=function(){this.__data__=[],this.size=0}},4125:(t,e,r)=>{var i=r(8470),n=Array.prototype.splice;t.exports=function(t){var e=this.__data__,r=i(e,t);return!(r<0||(r==e.length-1?e.pop():n.call(e,r,1),--this.size,0))}},2117:(t,e,r)=>{var i=r(8470);t.exports=function(t){var e=this.__data__,r=i(e,t);return r<0?void 0:e[r][1]}},7518:(t,e,r)=>{var i=r(8470);t.exports=function(t){return i(this.__data__,t)>-1}},4705:(t,e,r)=>{var i=r(8470);t.exports=function(t,e){var r=this.__data__,n=i(r,t);return n<0?(++this.size,r.push([t,e])):r[n][1]=e,this}},4785:(t,e,r)=>{var i=r(1989),n=r(8407),o=r(7071);t.exports=function(){this.size=0,this.__data__={hash:new i,map:new(o||n),string:new i}}},1285:(t,e,r)=>{var i=r(5050);t.exports=function(t){var e=i(this,t).delete(t);return this.size-=e?1:0,e}},6e3:(t,e,r)=>{var i=r(5050);t.exports=function(t){return i(this,t).get(t)}},9916:(t,e,r)=>{var i=r(5050);t.exports=function(t){return i(this,t).has(t)}},5265:(t,e,r)=>{var i=r(5050);t.exports=function(t,e){var r=i(this,t),n=r.size;return r.set(t,e),this.size+=r.size==n?0:1,this}},4523:(t,e,r)=>{var i=r(8306);t.exports=function(t){var e=i(t,(function(t){return 500===r.size&&r.clear(),t})),r=e.cache;return e}},4536:(t,e,r)=>{var i=r(852)(Object,"create");t.exports=i},2333:t=>{var e=Object.prototype.toString;t.exports=function(t){return e.call(t)}},5639:(t,e,r)=>{var i=r(1957),n="object"==typeof self&&self&&self.Object===Object&&self,o=i||n||Function("return this")();t.exports=o},5514:(t,e,r)=>{var i=r(4523),n=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,o=/\\(\\)?/g,s=i((function(t){var e=[];return 46===t.charCodeAt(0)&&e.push(""),t.replace(n,(function(t,r,i,n){e.push(i?n.replace(o,"$1"):r||t)})),e}));t.exports=s},327:(t,e,r)=>{var i=r(3448);t.exports=function(t){if("string"==typeof t||i(t))return t;var e=t+"";return"0"==e&&1/t==-1/0?"-0":e}},346:t=>{var e=Function.prototype.toString;t.exports=function(t){if(null!=t){try{return e.call(t)}catch(t){}try{return t+""}catch(t){}}return""}},7813:t=>{t.exports=function(t,e){return t===e||t!=t&&e!=e}},1469:t=>{var e=Array.isArray;t.exports=e},3560:(t,e,r)=>{var i=r(4239),n=r(3218);t.exports=function(t){if(!n(t))return!1;var e=i(t);return"[object Function]"==e||"[object GeneratorFunction]"==e||"[object AsyncFunction]"==e||"[object Proxy]"==e}},3218:t=>{t.exports=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},7005:t=>{t.exports=function(t){return null!=t&&"object"==typeof t}},3448:(t,e,r)=>{var i=r(4239),n=r(7005);t.exports=function(t){return"symbol"==typeof t||n(t)&&"[object Symbol]"==i(t)}},8306:(t,e,r)=>{var i=r(3369);function n(t,e){if("function"!=typeof t||null!=e&&"function"!=typeof e)throw new TypeError("Expected a function");var r=function(){var i=arguments,n=e?e.apply(this,i):i[0],o=r.cache;if(o.has(n))return o.get(n);var s=t.apply(this,i);return r.cache=o.set(n,s)||o,s};return r.cache=new(n.Cache||i),r}n.Cache=i,t.exports=n},6968:(t,e,r)=>{var i=r(611);t.exports=function(t,e,r){return null==t?t:i(t,e,r)}},9833:(t,e,r)=>{var i=r(531);t.exports=function(t){return null==t?"":i(t)}},6818:(t,e,r)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.deepSortObject=void 0;const i=r(6057),n=t=>{return!((0,i.isPlainObject)(t)||(e=t,Array.isArray(e)&&(!(e.length>0)||"object"==typeof e[0])));var e},o=([t,e],[r,i])=>n(e)&&n(i)?t.localeCompare(r):n(e)?-1:n(i)?1:t.localeCompare(r),s=(t,e)=>{let r;return Array.isArray(t)?t.map((function(t){return s(t,e)})):(0,i.isPlainObject)(t)?(r={},Object.entries(t).sort(e||o).forEach((function([t,i]){r[t]=s(i,e)})),r):t};e.deepSortObject=s},5563:(t,e,r)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Lib=void 0;const i=r(6818),n=r(1734);class o{static newHash(t,e){let r=(t=>{let e=0;for(let r=0;r{const e=new DataView(t);let r="";for(let t=0;t<5;t++){const i=e.getUint8(t);r+=String.fromCharCode(i)}return r},o.getIdFromSchema=t=>t.id,o.getIdFromModel=t=>t.schema.id},3134:(t,e,r)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Model=void 0;const i=r(488);class n extends i.Serialize{constructor(t,e=8){super(t,e),this.schema=t}}e.Model=n},1734:(t,e)=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Schema=void 0;class r{constructor(t,e,i){this._id=t,this._name=e,this._struct=i,this._bytes=0,r.Validation(i),this.calcBytes()}static Validation(t){}get id(){return this._id}get name(){return this._name}isSpecialType(t){return!Object.keys(t).filter((t=>"type"!=t&&"digits"!=t&&"length"!=t)).length}calcBytes(){const t=e=>{var r,i;for(let n in e){const o=(null==e?void 0:e._type)||!!this.isSpecialType(e)&&(null===(r=null==e?void 0:e.type)||void 0===r?void 0:r._type),s=(null==e?void 0:e._bytes)||!!this.isSpecialType(e)&&(null===(i=null==e?void 0:e.type)||void 0===i?void 0:i._bytes);if(!o&&e.hasOwnProperty(n))"object"==typeof e[n]&&t(e[n]);else{if("_type"!==n&&"type"!==n)continue;if(!s)continue;if("String8"===o||"String16"===o){const t=e.length||12;this._bytes+=s*t}else this._bytes+=s}}};t(this._struct)}get struct(){return this._struct}get bytes(){return this._bytes}}e.Schema=r},488:function(t,e,r){"use strict";var i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.Serialize=void 0;const n=r(5563),o=i(r(6968)),s=r(6818);e.Serialize=class{constructor(t,e){this.schema=t,this.bufferSize=e,this._buffer=new ArrayBuffer(0),this._dataView=new DataView(this._buffer),this._bytes=0}refresh(){this._buffer=new ArrayBuffer(1024*this.bufferSize),this._dataView=new DataView(this._buffer),this._bytes=0}cropString(t,e){return t.padEnd(e," ").slice(0,e)}isSpecialType(t){return!Object.keys(t).filter((t=>"type"!=t&&"digits"!=t&&"length"!=t)).length}boolArrayToInt(t){let e="1";for(var r=0;r>>0).toString(2)].map((t=>"0"!=t)).slice(1)}flatten(t,e){let r=[];const i=(t,e)=>{var n,o,s,a,l,u,c,p,y,h,_,f;let d;for(d in(null==t?void 0:t._id)?r.push({d:t._id,t:"String8"}):(null===(n=null==t?void 0:t[0])||void 0===n?void 0:n._id)&&r.push({d:t[0]._id,t:"String8"}),(null==t?void 0:t._struct)?t=t._struct:(null===(o=null==t?void 0:t[0])||void 0===o?void 0:o._struct)&&(t=t[0]._struct),e)if(e.hasOwnProperty(d))if("object"==typeof e[d])Array.isArray(e)?i(t,e[parseInt(d)]):"BitArray8"===(null===(s=t[d])||void 0===s?void 0:s._type)||"BitArray16"===(null===(a=t[d])||void 0===a?void 0:a._type)?r.push({d:this.boolArrayToInt(e[d]),t:t[d]._type}):i(t[d],e[d]);else if((null===(u=null===(l=t[d])||void 0===l?void 0:l.type)||void 0===u?void 0:u._type)&&this.isSpecialType(t[d])){if((null===(c=t[d])||void 0===c?void 0:c.digits)&&(e[d]*=Math.pow(10,t[d].digits),e[d]=parseInt(e[d].toFixed(0))),null===(p=t[d])||void 0===p?void 0:p.length){const r=null===(y=t[d])||void 0===y?void 0:y.length;e[d]=this.cropString(e[d],r)}r.push({d:e[d],t:t[d].type._type})}else(null===(h=t[d])||void 0===h?void 0:h._type)&&("String8"!==(null===(_=t[d])||void 0===_?void 0:_._type)&&"String16"!==(null===(f=t[d])||void 0===f?void 0:f._type)||(e[d]=this.cropString(e[d],12)),r.push({d:e[d],t:t[d]._type}))};return i(t,e),r}toBuffer(t){let e=(0,s.deepSortObject)(t);const r=JSON.parse(JSON.stringify(e));this.refresh(),this.flatten(this.schema,r).forEach(((t,e)=>{if("String8"===t.t)for(let e=0;e-1;)e=s.indexOf(35,e),-1!==e&&(r.push(e),e++);let a=[];r.forEach((t=>{let e="";for(let r=0;r<5;r++)e+=String.fromCharCode(s[t+r]);a.push(e)}));let l=[];a.forEach(((t,e)=>{n.Lib._schemas.get(t)&&l.push({id:t,schema:n.Lib._schemas.get(t),startsAt:r[e]+5})}));let u={},c=0,p={};const y=t=>{var e,r;let n={};if("object"==typeof t)for(let o in t)if(t.hasOwnProperty(o)){const s=t[o];let a;if(((null===(e=null==s?void 0:s.type)||void 0===e?void 0:e._type)||(null===(r=null==s?void 0:s.type)||void 0===r?void 0:r._bytes)||this.isSpecialType(s))&&(a=s,s._type=s.type._type,s._bytes=s.type._bytes),s&&s._type&&s._bytes){const t=s._type,e=s._bytes;let r;if("String8"===t){r="";const t=s.length||12;for(let e=0;e{var i,n,o;let s=null===(i=e.schema)||void 0===i?void 0:i.struct,a=e.startsAt,u=t.byteLength,h=(null===(n=e.schema)||void 0===n?void 0:n.id)||"XX";"XX"===h&&console.error("ERROR: Something went horribly wrong!");try{u=l[r+1].startsAt-5}catch{}const _=(null===(o=e.schema)||void 0===o?void 0:o.bytes)||1,f=(u-a)/_;for(let t=0;t{if(t&&t._id&&t._id===e){let t=i.replace(/_struct\./,"").replace(/\.$/,"");n&&!Array.isArray(r)&&(r=[r]),""===t?u={...u,...r}:(0,o.default)(u,t,r)}else for(const n in t)if(t.hasOwnProperty(n)&&"object"==typeof t[n]){let o=Array.isArray(t)?"":`${n}.`;h(t[n],e,r,i+o,Array.isArray(t))}};for(let t=0;t{"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.bool16=e.bool8=e.string16=e.string8=e.float64=e.float32=e.uint64=e.int64=e.uint32=e.int32=e.uint16=e.int16=e.uint8=e.int8=void 0,e.int8={_type:"Int8Array",_bytes:1},e.uint8={_type:"Uint8Array",_bytes:1},e.int16={_type:"Int16Array",_bytes:2},e.uint16={_type:"Uint16Array",_bytes:2},e.int32={_type:"Int32Array",_bytes:4},e.uint32={_type:"Uint32Array",_bytes:4},e.int64={_type:"BigInt64Array",_bytes:8},e.uint64={_type:"BigUint64Array",_bytes:8},e.float32={_type:"Float32Array",_bytes:4},e.float64={_type:"Float64Array",_bytes:8},e.string8={_type:"String8",_bytes:1},e.string16={_type:"String16",_bytes:2},e.bool8={_type:"BitArray8",_bytes:1},e.bool16={_type:"BitArray16",_bytes:2}}},e={};function r(i){var n=e[i];if(void 0!==n)return n.exports;var o=e[i]={exports:{}};return t[i].call(o.exports,o,o.exports,r),o.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}();var i={};(()=>{"use strict";var t=i;const e=r(5563),n=r(3134),o=r(7826);t.default={BufferSchema:e.Lib,Model:n.Model,int8:o.int8,uint8:o.uint8,int16:o.int16,uint16:o.uint16,int32:o.int32,uint32:o.uint32,int64:o.int64,uint64:o.uint64,float32:o.float32,float64:o.float64,string8:o.string8,string16:o.string16,bool8:o.bool8,bool16:o.bool16}})(),Schema=i.default})(); -------------------------------------------------------------------------------- /bundle/typed-array-buffer-schema.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * is-plain-object 3 | * 4 | * Copyright (c) 2014-2017, Jon Schlinkert. 5 | * Released under the MIT License. 6 | */ 7 | 8 | /** 9 | * @copyright 10 | * Copyright (c) 2014 IndigoUnited (https://github.com/IndigoUnited) 11 | * Copyright (c) 2021 Yannick Deubel (https://github.com/yandeu) 12 | * 13 | * @license {@link https://github.com/geckosio/geckos.io/blob/master/LICENSE BSD-3-Clause} 14 | * 15 | * @description 16 | * copied and modified from deep-sort-object@1.0.2 (https://github.com/IndigoUnited/js-deep-sort-object/blob/master/index.js) 17 | * previously licensed under MIT (https://github.com/IndigoUnited/js-deep-sort-object/blob/master/LICENSE) 18 | */ 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@geckos.io/typed-array-buffer-schema", 3 | "version": "1.2.1", 4 | "description": "A Schema based Object to Buffer converter", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "type": "commonjs", 8 | "scripts": { 9 | "test:ci": "npm i && npm run build && npm test && npm run bundle:tmp", 10 | "start": "npm run dev", 11 | "dev": "npm run build && npm-run-all --parallel dev:*", 12 | "dev:tsc": "tsc --watch", 13 | "dev:nodemon": "nodemon lib/dev.js --watch lib", 14 | "build": "npm run clean && tsc", 15 | "bundle": "webpack --config webpack.bundle.js", 16 | "bundle:tmp": "webpack --config webpack.bundle.tmp.js", 17 | "test": "jest --collectCoverage", 18 | "clean": "rimraf lib", 19 | "format": "prettier --write src/**/*.ts && prettier --write test/**/*.js", 20 | "preReleaseHook": "prepublishOnly", 21 | "prepublishOnly": "npm i && npm run build && npm test && npm run bundle" 22 | }, 23 | "keywords": [ 24 | "typed", 25 | "array", 26 | "buffer", 27 | "typedArray", 28 | "arrayBuffer", 29 | "serialize", 30 | "serialization", 31 | "schema", 32 | "binary" 33 | ], 34 | "author": "Yannick Deubel (https://github.com/yandeu)", 35 | "license": "BSD-3-Clause", 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/geckosio/typed-array-buffer-schema.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/geckosio/typed-array-buffer-schema/issues" 42 | }, 43 | "homepage": "http://geckos.io", 44 | "dependencies": { 45 | "is-plain-object": "^5.0.0", 46 | "lodash": "^4.17.21" 47 | }, 48 | "devDependencies": { 49 | "@types/lodash": "^4.14.177", 50 | "@yandeu/prettier-config": "^0.0.3", 51 | "jest": "^27.4.0", 52 | "nodemon": "^2.0.15", 53 | "npm-run-all": "^4.1.5", 54 | "rimraf": "^3.0.2", 55 | "ts-loader": "^9.2.6", 56 | "typescript": "^4.5.2", 57 | "underscore": "^1.13.4", 58 | "webpack": "^5.64.4", 59 | "webpack-cli": "^4.9.1" 60 | }, 61 | "funding": { 62 | "url": "https://github.com/sponsors/yandeu" 63 | } 64 | } -------------------------------------------------------------------------------- /readme/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geckosio/typed-array-buffer-schema/9827e7a2a72304a17202b5d393b277db082f1434/readme/logo.png -------------------------------------------------------------------------------- /src/bundle.ts: -------------------------------------------------------------------------------- 1 | import { Lib as BufferSchema } from './lib' 2 | import { Model } from './model' 3 | import { 4 | int8, 5 | uint8, 6 | int16, 7 | uint16, 8 | int32, 9 | uint32, 10 | int64, 11 | uint64, 12 | float32, 13 | float64, 14 | string8, 15 | string16, 16 | bool8, 17 | bool16 18 | } from './views' 19 | 20 | export default { 21 | BufferSchema, 22 | Model, 23 | int8, 24 | uint8, 25 | int16, 26 | uint16, 27 | int32, 28 | uint32, 29 | int64, 30 | uint64, 31 | float32, 32 | float64, 33 | string8, 34 | string16, 35 | bool8, 36 | bool16 37 | } 38 | -------------------------------------------------------------------------------- /src/deep-sort-object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 3 | * Copyright (c) 2014 IndigoUnited (https://github.com/IndigoUnited) 4 | * Copyright (c) 2021 Yannick Deubel (https://github.com/yandeu) 5 | * 6 | * @license {@link https://github.com/geckosio/geckos.io/blob/master/LICENSE BSD-3-Clause} 7 | * 8 | * @description 9 | * copied and modified from deep-sort-object@1.0.2 (https://github.com/IndigoUnited/js-deep-sort-object/blob/master/index.js) 10 | * previously licensed under MIT (https://github.com/IndigoUnited/js-deep-sort-object/blob/master/LICENSE) 11 | */ 12 | 13 | import { isPlainObject } from 'is-plain-object' 14 | 15 | const isPlainArray = (arr: any) => Array.isArray(arr) && (arr.length > 0 ? typeof arr[0] == "object" : true) 16 | 17 | const isValue = (val: any) => !isPlainObject(val) && !isPlainArray(val) 18 | 19 | /** Sort objects by key; sort properties that are not itself an object on top. */ 20 | // @ts-ignore 21 | const defaultSortFn = ([keyA, valueA], [keyB, valueB]) => { 22 | if (isValue(valueA) && isValue(valueB)) return keyA.localeCompare(keyB) 23 | if (isValue(valueA)) return -1 24 | if (isValue(valueB)) return 1 25 | 26 | return keyA.localeCompare(keyB) 27 | } 28 | 29 | const sort = (src: any, comparator?: any): any => { 30 | let out: any 31 | 32 | if (Array.isArray(src)) { 33 | return src.map(function (item) { 34 | return sort(item, comparator) 35 | }) 36 | } 37 | 38 | if (isPlainObject(src)) { 39 | out = {} 40 | 41 | Object.entries(src) 42 | .sort(comparator || defaultSortFn) 43 | .forEach(function ([key, value]) { 44 | out[key] = sort(value, comparator) 45 | }) 46 | 47 | return out 48 | } 49 | 50 | return src 51 | } 52 | 53 | export { sort as deepSortObject } 54 | -------------------------------------------------------------------------------- /src/dev.ts: -------------------------------------------------------------------------------- 1 | import { BufferSchema, Model, uint8, int16, uint16 } from './index' 2 | import { Schema } from './schema' 3 | import { string8, int64 } from './views' 4 | 5 | const playerSchema = BufferSchema.schema('player', { 6 | id: uint8, 7 | name: { type: string8, length: 6 }, 8 | x: { type: int16, digits: 2 }, 9 | y: { type: int16, digits: 2 } 10 | }) 11 | 12 | const towerSchema = BufferSchema.schema('tower', { 13 | id: uint8, 14 | health: uint8, 15 | team: uint8 16 | }) 17 | 18 | const mainSchema = BufferSchema.schema('snapshot', { 19 | time: int64, 20 | tick: uint16, 21 | players: [playerSchema], 22 | towers: [towerSchema] 23 | }) 24 | 25 | const gameState = { 26 | time: new Date().getTime(), 27 | tick: 32580, 28 | players: [ 29 | { id: 0, name: 'Mistin', x: -14.43, y: 47.78 }, 30 | { id: 1, name: 'Coobim', x: 21.85, y: -78.48 } 31 | ], 32 | towers: [ 33 | { id: 0, health: 100, team: 0 }, 34 | { id: 1, health: 89, team: 0 }, 35 | { id: 2, health: 45, team: 1 } 36 | ] 37 | } 38 | 39 | const mainModel = new Model(mainSchema) 40 | const buffer = mainModel.toBuffer(gameState) 41 | const data = mainModel.fromBuffer(buffer) 42 | 43 | // toBuffer() shrunk the byte size from 241 to only 56 44 | // that is -77% compression! 45 | console.log(JSON.stringify(gameState).length) // 241 46 | console.log(buffer.byteLength) // 56 47 | console.log(JSON.stringify(data).length) // 241 48 | 49 | //------------------------------------------------------------------ 50 | // Get the Schema IDs 51 | //------------------------------------------------------------------ 52 | const bufferId = BufferSchema.getIdFromBuffer(buffer) 53 | const schemaId = BufferSchema.getIdFromSchema(mainSchema) 54 | const modelId = BufferSchema.getIdFromModel(mainModel) 55 | 56 | console.log(`bufferId: ${bufferId}`) 57 | console.log(`schemaId: ${schemaId}`) 58 | console.log(`modelId: ${modelId}`) 59 | 60 | if (bufferId === schemaId && schemaId === modelId) console.log(`Schema name is "${mainSchema.name}"`) 61 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Lib as BufferSchema } from './lib' 2 | export { Model } from './model' 3 | export { int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64, string8, string16, bool8, bool16 } from './views' 4 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import { deepSortObject } from './deep-sort-object' 2 | import { Model } from './model' 3 | import { Schema } from './schema' 4 | 5 | export class Lib { 6 | public static _schemas: Map = new Map() 7 | 8 | public static newHash(name: string, _struct: any) { 9 | // https://stackoverflow.com/a/7616484/12656855 10 | const strToHash = (s: string) => { 11 | let hash = 0 12 | 13 | for (let i = 0; i < s.length; i++) { 14 | const chr = s.charCodeAt(i) 15 | hash = (hash << 5) - hash + chr 16 | hash |= 0 // Convert to 32bit integer 17 | } 18 | hash *= 254785 // times a random number 19 | return Math.abs(hash).toString(32).slice(2, 6) 20 | } 21 | 22 | let hash = strToHash(JSON.stringify(_struct) + name) 23 | if (hash.length !== 4) throw new Error('Hash has not length of 4') 24 | return `#${hash}` 25 | } 26 | 27 | public static schema(name: string, _struct: object) { 28 | _struct = deepSortObject(_struct as any) 29 | const id = Lib.newHash(name, _struct) 30 | const s = new Schema(id, name, _struct) 31 | this._schemas.set(id, s) 32 | return s 33 | } 34 | 35 | public static getIdFromBuffer = (buffer: ArrayBuffer) => { 36 | const dataView = new DataView(buffer) 37 | let id = '' 38 | 39 | for (let i = 0; i < 5; i++) { 40 | const uInt8 = dataView.getUint8(i) 41 | id += String.fromCharCode(uInt8) 42 | } 43 | 44 | return id 45 | } 46 | 47 | public static getIdFromSchema = (schema: Schema) => schema.id 48 | 49 | public static getIdFromModel = (model: Model) => model.schema.id 50 | } 51 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from './schema' 2 | import { Serialize } from './serialize' 3 | 4 | export class Model extends Serialize { 5 | /** 6 | * @param schema Your schema 7 | * @param bufferSize The max bufferSize in KB (default: 8) 8 | */ 9 | constructor(public schema: Schema, bufferSize = 8) { 10 | super(schema, bufferSize) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | //var property 2 | 3 | export class Schema { 4 | private _bytes: number = 0 5 | 6 | constructor(private _id: string, private _name: string, private _struct: Object) { 7 | Schema.Validation(_struct) 8 | this.calcBytes() 9 | } 10 | 11 | public static Validation(struct: Object) { 12 | // do all the validation here (as static me) 13 | } 14 | 15 | public get id() { 16 | return this._id 17 | } 18 | 19 | public get name() { 20 | return this._name 21 | } 22 | 23 | private isSpecialType(prop: object) { 24 | let propKeys = Object.keys(prop).filter(k => k != 'type' && k != 'digits' && k != 'length') 25 | 26 | return !propKeys.length 27 | } 28 | 29 | private calcBytes() { 30 | const iterate = (obj: any) => { 31 | for (let property in obj) { 32 | const type = obj?._type || (this.isSpecialType(obj) ? obj?.type?._type : false) 33 | const bytes = obj?._bytes || (this.isSpecialType(obj) ? obj?.type?._bytes : false) 34 | 35 | if (!type && obj.hasOwnProperty(property)) { 36 | if (typeof obj[property] === 'object') { 37 | iterate(obj[property]) 38 | } 39 | } 40 | //--- 41 | else { 42 | if (property !== '_type' && property !== 'type') continue 43 | if (!bytes) continue 44 | 45 | // we multiply the bytes by the String8 / String16 length. 46 | if (type === 'String8' || type === 'String16') { 47 | const length = obj.length || 12 48 | this._bytes += bytes * length 49 | } else { 50 | this._bytes += bytes 51 | } 52 | } 53 | } 54 | } 55 | iterate(this._struct) 56 | } 57 | 58 | public get struct() { 59 | return this._struct 60 | } 61 | 62 | public get bytes() { 63 | return this._bytes 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/serialize.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from './schema' 2 | import { Lib } from './lib' 3 | import set from 'lodash/set' 4 | import { deepSortObject } from './deep-sort-object' 5 | 6 | export class Serialize { 7 | protected _buffer: ArrayBuffer = new ArrayBuffer(0) 8 | protected _dataView: DataView = new DataView(this._buffer) 9 | protected _bytes: number = 0 10 | 11 | constructor(protected schema: Schema, private bufferSize: number) {} 12 | 13 | public refresh() { 14 | this._buffer = new ArrayBuffer(this.bufferSize * 1024) 15 | this._dataView = new DataView(this._buffer) 16 | this._bytes = 0 17 | } 18 | 19 | private cropString(str: string, length: number) { 20 | return str.padEnd(length, ' ').slice(0, length) 21 | } 22 | 23 | private isSpecialType(prop: object) { 24 | let propKeys = Object.keys(prop).filter(k => k != 'type' && k != 'digits' && k != 'length') 25 | 26 | return !propKeys.length 27 | } 28 | 29 | private boolArrayToInt(array: any) { 30 | // start with 1 to avoid errors such as 010 -> 10 31 | // now it will be 1010 which will not simplify 32 | let string = '1' 33 | for (var i = 0; i < array.length; i++) { 34 | string += +!!array[i] 35 | } 36 | return parseInt(string, 2) 37 | } 38 | 39 | private intToBoolArray(int: number) { 40 | // convert string to array, map the numbers to bools, 41 | // and remove the initial 1 42 | return [...(int >>> 0).toString(2)].map(e => (e == '0' ? false : true)).slice(1) 43 | } 44 | 45 | public flatten(schema: any, data: any) { 46 | let flat: any[] = [] 47 | 48 | // https://stackoverflow.com/a/15589677/12656855 49 | const flatten = (schema: any, data: any) => { 50 | // add the schema id to flat[] (its a String8 with 5 characters, the first char is #) 51 | if (schema?._id) flat.push({ d: schema._id, t: 'String8' }) 52 | else if (schema?.[0]?._id) flat.push({ d: schema[0]._id, t: 'String8' }) 53 | 54 | // if it is a schema 55 | if (schema?._struct) schema = schema._struct 56 | // if it is a schema[] 57 | else if (schema?.[0]?._struct) schema = schema[0]._struct 58 | 59 | // console.log('-------') 60 | // console.log('data', typeof data, data) 61 | 62 | let property: any 63 | for (property in data) { 64 | if (data.hasOwnProperty(property)) { 65 | if (typeof data[property] === 'object') { 66 | // if data is array, but schemas is flat, use index 0 on the next iteration 67 | if (Array.isArray(data)) { 68 | flatten(schema, data[parseInt(property)]) 69 | } else if (schema[property]?._type === 'BitArray8' || schema[property]?._type === 'BitArray16') { 70 | flat.push({ 71 | d: this.boolArrayToInt(data[property]), 72 | t: schema[property]._type 73 | }) 74 | } else flatten(schema[property], data[property]) 75 | } 76 | //--- 77 | else { 78 | // handle specialTypes e.g.: "x: { type: int16, digits: 2 }" 79 | if (schema[property]?.type?._type && this.isSpecialType(schema[property])) { 80 | if (schema[property]?.digits) { 81 | data[property] *= Math.pow(10, schema[property].digits) 82 | data[property] = parseInt(data[property].toFixed(0)) 83 | } 84 | if (schema[property]?.length) { 85 | const length = schema[property]?.length 86 | data[property] = this.cropString(data[property], length) 87 | } 88 | flat.push({ d: data[property], t: schema[property].type._type }) 89 | } else { 90 | if (schema[property]?._type) { 91 | // crop strings to default length of 12 characters if nothing else is specified 92 | if (schema[property]?._type === 'String8' || schema[property]?._type === 'String16') { 93 | data[property] = this.cropString(data[property], 12) 94 | } 95 | flat.push({ d: data[property], t: schema[property]._type }) 96 | } 97 | } 98 | } 99 | } else { 100 | } 101 | } 102 | } 103 | 104 | flatten(schema, data) 105 | 106 | return flat 107 | } 108 | 109 | public toBuffer(state: any) { 110 | let worldState = deepSortObject(state) 111 | 112 | // deep clone the worldState 113 | const data = JSON.parse(JSON.stringify(worldState)) 114 | 115 | this.refresh() 116 | 117 | const flat = this.flatten(this.schema, data) 118 | 119 | // to buffer 120 | flat.forEach((f: any, i: number) => { 121 | if (f.t === 'String8') { 122 | for (let j = 0; j < f.d.length; j++) { 123 | this._dataView.setUint8(this._bytes, f.d[j].charCodeAt(0)) 124 | this._bytes++ 125 | } 126 | } else if (f.t === 'String16') { 127 | for (let j = 0; j < f.d.length; j++) { 128 | this._dataView.setUint16(this._bytes, f.d[j].charCodeAt(0)) 129 | this._bytes += 2 130 | } 131 | } else if (f.t === 'Int8Array') { 132 | this._dataView.setInt8(this._bytes, f.d) 133 | this._bytes++ 134 | } else if (f.t === 'Uint8Array') { 135 | this._dataView.setUint8(this._bytes, f.d) 136 | this._bytes++ 137 | } else if (f.t === 'Int16Array') { 138 | this._dataView.setInt16(this._bytes, f.d) 139 | this._bytes += 2 140 | } else if (f.t === 'Uint16Array') { 141 | this._dataView.setUint16(this._bytes, f.d) 142 | this._bytes += 2 143 | } else if (f.t === 'Int32Array') { 144 | this._dataView.setInt32(this._bytes, f.d) 145 | this._bytes += 4 146 | } else if (f.t === 'Uint32Array') { 147 | this._dataView.setUint32(this._bytes, f.d) 148 | this._bytes += 4 149 | } else if (f.t === 'BigInt64Array') { 150 | this._dataView.setBigInt64(this._bytes, BigInt(f.d)) 151 | this._bytes += 8 152 | } else if (f.t === 'BigUint64Array') { 153 | this._dataView.setBigUint64(this._bytes, BigInt(f.d)) 154 | this._bytes += 8 155 | } else if (f.t === 'Float32Array') { 156 | this._dataView.setFloat32(this._bytes, f.d) 157 | this._bytes += 4 158 | } else if (f.t === 'Float64Array') { 159 | this._dataView.setFloat64(this._bytes, f.d) 160 | this._bytes += 8 161 | } else if (f.t === 'BitArray8') { 162 | this._dataView.setUint8(this._bytes, f.d) 163 | this._bytes++ 164 | } else if (f.t === 'BitArray16') { 165 | this._dataView.setUint16(this._bytes, f.d) 166 | this._bytes += 2 167 | } else { 168 | console.log('ERROR: Something unexpected happened!') 169 | } 170 | }) 171 | 172 | const newBuffer = new ArrayBuffer(this._bytes) 173 | const view = new DataView(newBuffer) 174 | 175 | // copy all data to a new (resized) ArrayBuffer 176 | for (let i = 0; i < this._bytes; i++) { 177 | view.setUint8(i, this._dataView.getUint8(i)) 178 | } 179 | 180 | return newBuffer 181 | } 182 | 183 | public fromBuffer(buffer: ArrayBuffer) { 184 | // 35 is # 185 | 186 | // check where, in the buffer, the schemas are 187 | let index = 0 188 | let indexes: number[] = [] 189 | 190 | const view = new DataView(buffer) 191 | const int8 = Array.from(new Int8Array(buffer)) 192 | 193 | while (index > -1) { 194 | index = int8.indexOf(35, index) 195 | if (index !== -1) { 196 | indexes.push(index) 197 | index++ 198 | } 199 | } 200 | 201 | // get the schema ids 202 | let schemaIds: string[] = [] 203 | indexes.forEach(index => { 204 | let id = '' 205 | for (let i = 0; i < 5; i++) { 206 | let char = String.fromCharCode(int8[index + i]) 207 | id += char 208 | } 209 | schemaIds.push(id) 210 | }) 211 | 212 | // assemble all info about the schemas we need 213 | let schemas: { id: string; schema: any; startsAt: number }[] = [] 214 | schemaIds.forEach((id, i) => { 215 | // check if the schemaId exists 216 | // (this can be, for example, if charCode 35 is not really a #) 217 | const schemaId = Lib._schemas.get(id) 218 | if (schemaId) schemas.push({ id, schema: Lib._schemas.get(id), startsAt: indexes[i] + 5 }) 219 | }) 220 | // schemas[] contains now all the schemas we need to fromBuffer the bufferArray 221 | 222 | // lets begin the serialization 223 | let data: any = {} // holds all the data we want to give back 224 | let bytes: number = 0 // the current bytes of arrayBuffer iteration 225 | let dataPerSchema: any = {} 226 | 227 | const deserializeSchema = (struct: any) => { 228 | let data = {} 229 | if (typeof struct === 'object') { 230 | for (let property in struct) { 231 | if (struct.hasOwnProperty(property)) { 232 | const prop = struct[property] 233 | 234 | // handle specialTypes e.g.: "x: { type: int16, digits: 2 }" 235 | let specialTypes 236 | if (prop?.type?._type || prop?.type?._bytes || this.isSpecialType(prop)) { 237 | specialTypes = prop 238 | prop._type = prop.type._type 239 | prop._bytes = prop.type._bytes 240 | } 241 | 242 | if (prop && prop['_type'] && prop['_bytes']) { 243 | const _type = prop['_type'] 244 | const _bytes = prop['_bytes'] 245 | let value 246 | 247 | if (_type === 'String8') { 248 | value = '' 249 | const length = prop.length || 12 250 | for (let i = 0; i < length; i++) { 251 | const char = String.fromCharCode(view.getUint8(bytes)) 252 | value += char 253 | bytes++ 254 | } 255 | } 256 | if (_type === 'String16') { 257 | value = '' 258 | const length = prop.length || 12 259 | for (let i = 0; i < length; i++) { 260 | const char = String.fromCharCode(view.getUint16(bytes)) 261 | value += char 262 | bytes += 2 263 | } 264 | } 265 | if (_type === 'Int8Array') { 266 | value = view.getInt8(bytes) 267 | bytes += _bytes 268 | } 269 | if (_type === 'Uint8Array') { 270 | value = view.getUint8(bytes) 271 | bytes += _bytes 272 | } 273 | if (_type === 'Int16Array') { 274 | value = view.getInt16(bytes) 275 | bytes += _bytes 276 | } 277 | if (_type === 'Uint16Array') { 278 | value = view.getUint16(bytes) 279 | bytes += _bytes 280 | } 281 | if (_type === 'Int32Array') { 282 | value = view.getInt32(bytes) 283 | bytes += _bytes 284 | } 285 | if (_type === 'Uint32Array') { 286 | value = view.getUint32(bytes) 287 | bytes += _bytes 288 | } 289 | if (_type === 'BigInt64Array') { 290 | value = parseInt(view.getBigInt64(bytes).toString()) 291 | bytes += _bytes 292 | } 293 | if (_type === 'BigUint64Array') { 294 | value = parseInt(view.getBigUint64(bytes).toString()) 295 | bytes += _bytes 296 | } 297 | if (_type === 'Float32Array') { 298 | value = view.getFloat32(bytes) 299 | bytes += _bytes 300 | } 301 | if (_type === 'Float64Array') { 302 | value = view.getFloat64(bytes) 303 | bytes += _bytes 304 | } 305 | if (_type === 'BitArray8') { 306 | value = this.intToBoolArray(view.getUint8(bytes)) 307 | bytes += _bytes 308 | } 309 | if (_type === 'BitArray16') { 310 | value = this.intToBoolArray(view.getUint16(bytes)) 311 | bytes += _bytes 312 | } 313 | 314 | // apply special types options 315 | if (typeof value === 'number' && specialTypes?.digits) { 316 | value *= Math.pow(10, -specialTypes.digits) 317 | value = parseFloat(value.toFixed(specialTypes.digits)) 318 | } 319 | 320 | data = { ...data, [property]: value } 321 | } 322 | } 323 | } 324 | } 325 | return data 326 | } 327 | 328 | schemas.forEach((s, i) => { 329 | let struct = s.schema?.struct 330 | let start = s.startsAt 331 | let end = buffer.byteLength 332 | let id = s.schema?.id || 'XX' 333 | 334 | if (id === 'XX') console.error('ERROR: Something went horribly wrong!') 335 | 336 | try { 337 | end = schemas[i + 1].startsAt - 5 338 | } catch {} 339 | 340 | // TOOD(yandeu) bytes is not accurate since it includes child schemas 341 | const length = s.schema?.bytes || 1 342 | // determine how many iteration we have to make in this schema 343 | // the players array maybe contains 5 player, so we have to make 5 iterations 344 | const iterations = (end - start) / length 345 | 346 | for (let i = 0; i < iterations; i++) { 347 | bytes = start + i * length 348 | // gets the data from this schema 349 | let schemaData = deserializeSchema(struct) 350 | 351 | if (iterations <= 1) dataPerSchema[id] = { ...schemaData } 352 | else { 353 | if (typeof dataPerSchema[id] === 'undefined') dataPerSchema[id] = [] 354 | dataPerSchema[id].push(schemaData) 355 | } 356 | } 357 | }) 358 | 359 | // add dataPerScheme to data 360 | data = {} 361 | 362 | const populateData = (obj: any, key: any, value: any, path: string = '', isArray = false) => { 363 | if (obj && obj._id && obj._id === key) { 364 | let p = path.replace(/_struct\./, '').replace(/\.$/, '') 365 | // if it is a schema[], but only has one set, we manually have to make sure it transforms to an array 366 | if (isArray && !Array.isArray(value)) value = [value] 367 | // '' is the top level 368 | if (p === '') data = { ...data, ...value } 369 | else set(data, p, value) 370 | } else { 371 | for (const props in obj) { 372 | if (obj.hasOwnProperty(props)) { 373 | if (typeof obj[props] === 'object') { 374 | let p = Array.isArray(obj) ? '' : `${props}.` 375 | populateData(obj[props], key, value, path + p, Array.isArray(obj)) 376 | } 377 | //obj 378 | } 379 | } 380 | } 381 | } 382 | 383 | // to it backwards (don't remember why this is needed, but it works without it) 384 | // for (let i = Object.keys(dataPerSchema).length - 1; i >= 0; i--) { 385 | // const key = Object.keys(dataPerSchema)[i] 386 | // const value = dataPerSchema[key] 387 | // populateData(this.schema, key, value, '') 388 | // } 389 | 390 | for (let i = 0; i < Object.keys(dataPerSchema).length; i++) { 391 | const key = Object.keys(dataPerSchema)[i] 392 | const value = dataPerSchema[key] 393 | populateData(this.schema, key, value, '') 394 | } 395 | 396 | return data 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/views.ts: -------------------------------------------------------------------------------- 1 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays 2 | 3 | /** -128 to 127 (1 byte) */ 4 | export const int8 = { _type: 'Int8Array', _bytes: 1 } 5 | /** 0 to 255 (1 byte) */ 6 | export const uint8 = { _type: 'Uint8Array', _bytes: 1 } 7 | 8 | /** -32768 to 32767 (2 bytes) */ 9 | export const int16 = { _type: 'Int16Array', _bytes: 2 } 10 | /** 0 to 65535 (2 bytes) */ 11 | export const uint16 = { _type: 'Uint16Array', _bytes: 2 } 12 | 13 | /** -2147483648 to 2147483647 (4 bytes) */ 14 | export const int32 = { _type: 'Int32Array', _bytes: 4 } 15 | /** 0 to 4294967295 (4 bytes) */ 16 | export const uint32 = { _type: 'Uint32Array', _bytes: 4 } 17 | 18 | /** -2^63 to 2^63-1 (8 bytes) */ 19 | export const int64 = { _type: 'BigInt64Array', _bytes: 8 } 20 | /** 0 to 2^64-1 (8 bytes) */ 21 | export const uint64 = { _type: 'BigUint64Array', _bytes: 8 } 22 | 23 | /** 1.2×10-38 to 3.4×1038 (7 significant digits e.g., 1.123456) (4 bytes) */ 24 | export const float32 = { _type: 'Float32Array', _bytes: 4 } 25 | 26 | /** 5.0×10-324 to 1.8×10308 (16 significant digits e.g., 1.123...15) (8 bytes) */ 27 | export const float64 = { _type: 'Float64Array', _bytes: 8 } 28 | 29 | /** 1 byte per character */ 30 | export const string8 = { _type: 'String8', _bytes: 1 } 31 | /** 2 bytes per character */ 32 | export const string16 = { _type: 'String16', _bytes: 2 } 33 | 34 | /** An array of 7 booleans */ 35 | export const bool8 = { _type: 'BitArray8', _bytes: 1 } 36 | /** An array of 15 booleans */ 37 | export const bool16 = { _type: 'BitArray16', _bytes: 2 } 38 | -------------------------------------------------------------------------------- /test/benchmark.test.js: -------------------------------------------------------------------------------- 1 | const { BufferSchema, Model, uint8, int8, int16, uint16 } = require('../lib/index.js') 2 | 3 | describe('simple test', () => { 4 | const playerSchema = BufferSchema.schema('player', { 5 | id: int8, 6 | x: int16, 7 | y: int16 8 | }) 9 | 10 | const snapshotSchema = BufferSchema.schema('snapshot', { 11 | time: uint16, 12 | data: { 13 | players: [playerSchema] 14 | } 15 | }) 16 | 17 | const SnapshotModel = new Model(snapshotSchema) 18 | 19 | const snap = { 20 | time: 1234, 21 | data: { 22 | players: [ 23 | { id: 0, x: 22, y: 38 }, 24 | { id: 1, x: -54, y: 7 } 25 | ] 26 | } 27 | } 28 | 29 | let buffer 30 | let data = snap 31 | 32 | test('should convert as many time as possible', () => { 33 | const hrstart = process.hrtime() 34 | 35 | for (let i = 0; i < 100; i++) { 36 | buffer = SnapshotModel.toBuffer(data) 37 | data = SnapshotModel.fromBuffer(buffer) 38 | } 39 | 40 | const hrend = process.hrtime(hrstart) 41 | 42 | console.info('Execution time (hr): %ds %dms', hrend[0], hrend[1] / 1000000) 43 | 44 | const dataL = JSON.stringify(data).length 45 | const snapL = JSON.stringify(snap).length 46 | 47 | expect(dataL).toBe(snapL) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/dataViews.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | BufferSchema, 3 | Model, 4 | int8, 5 | uint8, 6 | int16, 7 | uint16, 8 | int32, 9 | uint32, 10 | int64, 11 | uint64, 12 | float32, 13 | float64, 14 | string8, 15 | string16, 16 | bool8, 17 | bool16 18 | } = require('../lib/index.js') 19 | 20 | describe('dataViews test', () => { 21 | const playerSchema = BufferSchema.schema('player', { 22 | a: int8, 23 | b: uint8, 24 | c: int16, 25 | d: uint16, 26 | e: int32, 27 | f: uint32, 28 | g: int64, 29 | h: uint64, 30 | i: float32, 31 | j: float64, 32 | k: string8, 33 | kk: { type: string8, length: 24 }, 34 | l: string16, 35 | m: bool8, 36 | n: bool16 37 | }) 38 | 39 | const snapshotSchema = BufferSchema.schema('snapshot', { 40 | players: [playerSchema] 41 | }) 42 | 43 | const SnapshotModel = new Model(snapshotSchema) 44 | 45 | const now = new Date().getTime() 46 | 47 | const snap = { 48 | players: [ 49 | { 50 | a: 10, 51 | b: 10, 52 | c: 50, 53 | d: 50, 54 | e: 100, 55 | f: 100, 56 | g: now, 57 | h: now, 58 | i: 1.123456, 59 | j: 1.123456789, 60 | k: 'This line is too long.', 61 | kk: 'This line is too long.', 62 | l: 'Эта строка слишком длинная.', 63 | m: [true, false, false], 64 | n: [true, true, false, true, true, true, false, false, false, true] 65 | } 66 | ] 67 | } 68 | 69 | let buffer 70 | let data = snap 71 | 72 | test('should convert successfully', () => { 73 | buffer = SnapshotModel.toBuffer(data) 74 | data = SnapshotModel.fromBuffer(buffer) 75 | 76 | expect(data.players[0].m[2]).toBe(false) 77 | expect(data.players[0].n[7]).toBe(false) 78 | expect(data.players[0].g).toBe(now) 79 | expect(data.players[0].h).toBe(now) 80 | expect(data.players[0].k).toBe('This line is') 81 | expect(data.players[0].kk.trim()).toBe('This line is too long.') 82 | expect(data.players[0].l).toBe('Эта строка с') 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/emptyData.test.js: -------------------------------------------------------------------------------- 1 | const { BufferSchema, Model, uint8, int16, uint16 } = require('../lib/index.js') 2 | 3 | describe('simple test', () => { 4 | const playerSchema = BufferSchema.schema('player', { 5 | id: uint8 6 | }) 7 | 8 | const botSchema = BufferSchema.schema('bot', { 9 | id: uint8 10 | }) 11 | 12 | const carSchema = BufferSchema.schema('car', { 13 | id: uint8 14 | }) 15 | 16 | const snapshotSchema = BufferSchema.schema('snapshot', { 17 | time: uint16, 18 | data: { 19 | emptyArr: [playerSchema], 20 | emptyObj: botSchema, 21 | superCar: carSchema 22 | } 23 | }) 24 | 25 | const SnapshotModel = new Model(snapshotSchema) 26 | 27 | const snap = { 28 | data: { 29 | emptyArr: [], 30 | emptyObj: {}, 31 | superCar: { 32 | id: 911 33 | } 34 | } 35 | } 36 | 37 | test('empty arrays and empty object are omitted', () => { 38 | const buffer = SnapshotModel.toBuffer(snap) 39 | 40 | const dataL = JSON.stringify(SnapshotModel.fromBuffer(buffer)).length 41 | const snapL = JSON.stringify(snap).length 42 | const emptiesL = '"emptyArr":[],"emptyObj":{},'.length 43 | 44 | expect(dataL).toBe(snapL - emptiesL) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/schemaId.test.js: -------------------------------------------------------------------------------- 1 | const { BufferSchema, Model, uint8, int16, uint16 } = require('../lib/index.js') 2 | const { Schema } = require('../lib/schema.js') 3 | 4 | describe('get schema id test', () => { 5 | const schema = BufferSchema.schema('mySchema', { 6 | id: uint8, 7 | x: { type: uint16, digits: 4 } 8 | }) 9 | 10 | const model = new Model(schema) 11 | const state = { id: 0, x: 1.2345 } 12 | const buffer = model.toBuffer(state) 13 | 14 | test('should get the same ids', () => { 15 | const bufferId = BufferSchema.getIdFromBuffer(buffer) 16 | const schemaId = BufferSchema.getIdFromSchema(schema) 17 | const modelId = BufferSchema.getIdFromModel(model) 18 | 19 | expect(bufferId).toBe(schemaId) 20 | expect(schemaId).toBe(modelId) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/serializeDeserialize.test.js: -------------------------------------------------------------------------------- 1 | const { BufferSchema, Model, uint8, uint32, string8, int16, bool8 } = require('../lib/index.js') 2 | 3 | // see: https://github.com/geckosio/typed-array-buffer-schema/issues/7 4 | describe('serialize deserialize', () => { 5 | const movementSchema = BufferSchema.schema('movement', { 6 | sequenceNumber: uint32, 7 | horizontal: uint8, 8 | vertical: uint8, 9 | options: bool8 10 | }) 11 | const movementModel = new Model(movementSchema) 12 | 13 | const inOrder = { 14 | sequenceNumber: 2, 15 | horizontal: 4, 16 | vertical: 4, 17 | options: [false, true, false] 18 | } 19 | const notInOrder = { 20 | horizontal: 4, 21 | options: [false, true, false], 22 | vertical: 4, 23 | sequenceNumber: 2 24 | } 25 | 26 | it('should work if defined in order', () => { 27 | const serialized = movementModel.toBuffer(inOrder) 28 | const deserialized = movementModel.fromBuffer(serialized) 29 | 30 | expect(deserialized.sequenceNumber).toBe(2) 31 | expect(deserialized.horizontal).toBe(4) 32 | expect(deserialized.vertical).toBe(4) 33 | expect(deserialized.options[1]).toBe(true) 34 | }) 35 | 36 | it('should work if NOT defined in order', () => { 37 | const serialized = movementModel.toBuffer(notInOrder) 38 | const deserialized = movementModel.fromBuffer(serialized) 39 | 40 | expect(deserialized.sequenceNumber).toBe(2) 41 | expect(deserialized.horizontal).toBe(4) 42 | expect(deserialized.vertical).toBe(4) 43 | expect(deserialized.options[1]).toBe(true) 44 | }) 45 | }) 46 | 47 | describe('serialize deserialize (complex)', () => { 48 | const TimerSchema = BufferSchema.schema('timer', { 49 | time: uint32 50 | }) 51 | 52 | const castleSchema = BufferSchema.schema('castle', { 53 | name: string8, 54 | health: uint8 55 | }) 56 | 57 | const playerSchema = BufferSchema.schema('player', { 58 | id: uint8, 59 | y: int16, 60 | x: int16 61 | }) 62 | 63 | const gameSchema = BufferSchema.schema('snapshot', { 64 | name: string8, 65 | players: [playerSchema], 66 | time: uint32, 67 | stats: TimerSchema, 68 | castles: [castleSchema], 69 | config: bool8 70 | }) 71 | 72 | const gameModel = new Model(gameSchema) 73 | 74 | const timeInSeconds = Math.floor(new Date().getTime() / 1000) 75 | 76 | const randomOrder = { 77 | stats: { time: timeInSeconds }, 78 | time: timeInSeconds, 79 | castles: [ 80 | { name: 'beauty', health: 100 }, 81 | { health: 78, name: 'beauty2' }, 82 | { health: 88, name: 'very_long_name' } 83 | ], 84 | name: 'myGame', 85 | players: [ 86 | { id: 25, x: 788, y: -14 }, 87 | { x: 1, y: 2, id: 87 } 88 | ], 89 | config: [true, false, true, true, true] 90 | } 91 | 92 | it('should work if defined randomly', () => { 93 | const serialized = gameModel.toBuffer(randomOrder) 94 | const deserialized = gameModel.fromBuffer(serialized) 95 | 96 | expect(deserialized.players[0].id).toBe(25) 97 | expect(deserialized.castles[1].name.trim()).toBe('beauty2') 98 | expect(deserialized.castles[2].name.trim()).toBe('very_long_name'.slice(0, 12)) 99 | expect(deserialized.name.trim()).toBe('myGame') 100 | expect(deserialized.time).toBe(timeInSeconds) 101 | expect(deserialized.stats.time).toBe(timeInSeconds) 102 | expect(deserialized.config[1]).toBe(false) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/simple.test.js: -------------------------------------------------------------------------------- 1 | const { BufferSchema, Model, uint8, int16, uint16, string8, bool8 } = require('../lib/index.js') 2 | const _ = require('underscore') 3 | 4 | describe('simple test', () => { 5 | const castleSchema = BufferSchema.schema('castle', { 6 | id: uint8, 7 | type: { type: string8, length: 3 }, 8 | health: uint8 9 | }) 10 | 11 | const playerSchema = BufferSchema.schema('player', { 12 | id: uint8, 13 | a: { type: int16, digits: 1 }, 14 | b: { type: int16, digits: 1 }, 15 | x: int16, 16 | y: int16 17 | }) 18 | 19 | const listSchema = BufferSchema.schema('list', { 20 | value: uint8 21 | }) 22 | 23 | const snapshotSchema = BufferSchema.schema('snapshot', { 24 | time: uint16, 25 | single: uint8, 26 | data: { list: [listSchema], players: [playerSchema], castles: [castleSchema] }, 27 | serverConfig: bool8 28 | }) 29 | 30 | const SnapshotModel = new Model(snapshotSchema) 31 | 32 | const snap = { 33 | time: 1234, 34 | single: 0, 35 | data: { 36 | list: [{ value: 1 }, { value: 2 }], 37 | castles: [ 38 | { 39 | id: 2, 40 | type: 'big', 41 | health: 81 42 | } 43 | ], 44 | players: [ 45 | { 46 | id: 14, 47 | a: 10, 48 | b: 5, 49 | x: 145, 50 | y: 98 51 | }, 52 | { 53 | id: 15, 54 | a: 7, 55 | b: -55, 56 | x: 218, 57 | y: -14 58 | } 59 | ] 60 | }, 61 | serverConfig: [true, false, true, false, false] 62 | } 63 | 64 | let buffer 65 | let data 66 | 67 | test('get schema name', () => { 68 | expect(castleSchema.name).toBe('castle') 69 | }) 70 | 71 | test('should return a buffer', () => { 72 | buffer = SnapshotModel.toBuffer(snap) 73 | const uint8 = new Uint8Array(buffer) 74 | 75 | expect(typeof buffer).toBe('object') 76 | expect(uint8.buffer.byteLength).toBe(49) 77 | }) 78 | 79 | test('should fromBuffer', () => { 80 | data = SnapshotModel.fromBuffer(buffer) 81 | 82 | expect(data.time).toBe(1234) 83 | expect(data.data.players[0].x).toBe(145) 84 | expect(data.data.players[0].a).toBe(10) 85 | expect(data.data.players[1].b).toBe(-55) 86 | }) 87 | 88 | test('stringified version should have same length', () => { 89 | expect(_.isEqual(snap, data)).toBeTruthy() 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /test/specialTypes.test.js: -------------------------------------------------------------------------------- 1 | const { BufferSchema, Model, uint8, int16, uint16 } = require('../lib/index.js') 2 | 3 | describe('special types test', () => { 4 | const playerSchema = BufferSchema.schema('player', { 5 | id: uint8, 6 | x: { type: uint16, digits: 4 } 7 | }) 8 | 9 | const PlayerModel = new Model(playerSchema) 10 | 11 | const state = { id: 0, x: 5.211427545 } 12 | 13 | test('should be able to manage and crop digits', () => { 14 | const buffer = PlayerModel.toBuffer(state) 15 | const data = PlayerModel.fromBuffer(buffer) 16 | 17 | expect(data.x).toBe(5.2114) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | 7 | "rootDir": "src", 8 | "outDir": "lib", 9 | 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "newLine": "lf", 13 | 14 | "sourceMap": true, 15 | "declaration": true, 16 | "declarationMap": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "**/*.spec.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /webpack.bundle.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'production', 5 | entry: './src/bundle.ts', 6 | output: { 7 | filename: 'typed-array-buffer-schema.js', 8 | path: path.resolve(__dirname, 'bundle'), 9 | library: 'Schema', 10 | libraryExport: 'default' 11 | }, 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js'] 14 | }, 15 | module: { 16 | rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }] 17 | } 18 | } -------------------------------------------------------------------------------- /webpack.bundle.tmp.js: -------------------------------------------------------------------------------- 1 | const config = require('./webpack.bundle.js') 2 | 3 | module.exports = { ...config, output: { ...config.output, filename: 'typed-array-buffer-schema.tmp.js' } } 4 | --------------------------------------------------------------------------------