Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@colyseus/schema

Package Overview
Dependencies
Maintainers
1
Versions
313
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@colyseus/schema - npm Package Compare versions

Comparing version 0.3.2 to 0.3.3

lib/ChangeTracker.d.ts

31

lib/annotations.d.ts

@@ -1,2 +0,2 @@

import * as decode from "./msgpack/decode";
import * as decode from "./encoding/decode";
import { ArraySchema } from './types/ArraySchema';

@@ -13,2 +13,6 @@ /**

};
export declare type FilterCallback = (this: Schema, client: Client, instance: Schema, root?: Schema) => boolean;
export declare type Client = {
sessionId: string;
} & any;
export interface DataChange<T = any> {

@@ -27,2 +31,6 @@ field: string;

};
static _filters: {
[field: string]: FilterCallback;
};
static _descriptors: PropertyDescriptorMap & ThisType<any>;
$changed: boolean;

@@ -41,10 +49,17 @@ protected $allChanges: {

constructor(...args: any[]);
markAsChanged(field: string, value?: Schema | any): void;
readonly _schema: Definition;
readonly _descriptors: PropertyDescriptorMap & ThisType<any>;
readonly _indexes: {
[field: string]: number;
};
readonly _filters: {
[field: string]: FilterCallback;
};
markAsChanged(field: string, value?: Schema | any): void;
markAsUnchanged(): void;
decode(bytes: any, it?: decode.Iterator): this;
encode(root?: boolean, encodeAll?: boolean): any[];
encode(root?: Schema, encodeAll?: boolean, client?: Client): any[];
encodeFiltered(client: Client): any[];
encodeAll(): any[];
encodeAllFiltered(client: Client): any[];
toJSON(): {};

@@ -67,7 +82,11 @@ }

static encode(instance: Schema): any[];
static decode(bytes: number[]): any;
static decode(bytes: number[]): Schema;
}
/**
* Decorators / Proxies
* `@type()` decorator for proxies
*/
export declare function type(type: DefinitionType): (target: any, field: string) => void;
export declare function type(type: DefinitionType): PropertyDecorator;
/**
* `@filter()` decorator for defining data filters per client
*/
export declare function filter(cb: FilterCallback): PropertyDecorator;

288

lib/annotations.js

@@ -23,4 +23,4 @@ "use strict";

var spec_1 = require("./spec");
var encode = require("./msgpack/encode");
var decode = require("./msgpack/decode");
var encode = require("./encoding/encode");
var decode = require("./encoding/decode");
var ArraySchema_1 = require("./types/ArraySchema");

@@ -57,6 +57,36 @@ var MapSchema_1 = require("./types/MapSchema");

}
this.$changed = false;
this.$allChanges = {};
this.$changes = {};
// fix enumerability of fields for end-user
Object.defineProperties(this, {
$changed: { value: false, enumerable: false, writable: true },
$changes: { value: {}, enumerable: false, writable: true },
$allChanges: { value: {}, enumerable: false, writable: true },
$parent: { value: undefined, enumerable: false, writable: true },
$parentField: { value: undefined, enumerable: false, writable: true },
$parentIndexChange: { value: undefined, enumerable: false, writable: true },
});
var descriptors = this._descriptors;
if (descriptors) {
Object.defineProperties(this, descriptors);
}
}
Object.defineProperty(Schema.prototype, "_schema", {
get: function () { return this.constructor._schema; },
enumerable: true,
configurable: true
});
Object.defineProperty(Schema.prototype, "_descriptors", {
get: function () { return this.constructor._descriptors; },
enumerable: true,
configurable: true
});
Object.defineProperty(Schema.prototype, "_indexes", {
get: function () { return this.constructor._indexes; },
enumerable: true,
configurable: true
});
Object.defineProperty(Schema.prototype, "_filters", {
get: function () { return this.constructor._filters; },
enumerable: true,
configurable: true
});
Schema.prototype.markAsChanged = function (field, value) {

@@ -66,5 +96,6 @@ var fieldSchema = this._schema[field];

if (value !== undefined) {
if (Array.isArray(value.$parentField) ||
if (value &&
Array.isArray(value.$parentField) ||
fieldSchema && (Array.isArray(fieldSchema) || fieldSchema.map)) {
var $parentField = value.$parentField || [];
var $parentField = value && value.$parentField || [];
// used for MAP/ARRAY

@@ -93,3 +124,3 @@ var fieldName = ($parentField.length > 0)

}
else if (value.$parentField) {
else if (value && value.$parentField) {
// used for direct type relationship

@@ -109,16 +140,40 @@ this.$changes[value.$parentField] = value;

};
Object.defineProperty(Schema.prototype, "_schema", {
get: function () {
return this.constructor._schema;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Schema.prototype, "_indexes", {
get: function () {
return this.constructor._indexes;
},
enumerable: true,
configurable: true
});
Schema.prototype.markAsUnchanged = function () {
var schema = this._schema;
var changes = this.$changes;
for (var field in changes) {
var type_1 = schema[field];
var value = changes[field];
// skip unchagned fields
if (value === undefined) {
continue;
}
if (type_1._schema) {
value.markAsUnchanged();
}
else if (Array.isArray(type_1)) {
// encode Array of type
for (var i = 0, l = value.length; i < l; i++) {
var index = value[i];
var item = this["_" + field][index];
if (typeof (type_1[0]) !== "string") { // is array of Schema
item.markAsUnchanged();
}
}
}
else if (type_1.map) {
var keys = value;
var mapKeys = Object.keys(this["_" + field]);
for (var i = 0; i < keys.length; i++) {
var key = mapKeys[keys[i]] || keys[i];
var item = this["_" + field][key];
if (item instanceof Schema) {
item.markAsUnchanged();
}
}
}
}
this.$changed = false;
this.$changes = {};
};
Schema.prototype.decode = function (bytes, it) {

@@ -137,18 +192,24 @@ if (it === void 0) { it = { offset: 0 }; }

var index = bytes[it.offset++];
var field = fieldsByIndex[index];
if (index === spec_1.END_OF_STRUCTURE) {
return "break";
}
var type_1 = schema[field];
var field = fieldsByIndex[index];
var type_2 = schema[field];
var value = void 0;
var change = void 0; // for triggering onChange
var hasChange = false;
if (type_1._schema) {
value = this_1["_" + field] || new type_1();
value.$parent = this_1;
value.decode(bytes, it);
if (type_2._schema) {
if (decode.nilCheck(bytes, it)) {
it.offset++;
value = null;
}
else {
value = this_1["_" + field] || new type_2();
value.$parent = this_1;
value.decode(bytes, it);
}
hasChange = true;
}
else if (Array.isArray(type_1)) {
type_1 = type_1[0];
else if (Array.isArray(type_2)) {
type_2 = type_2[0];
change = [];

@@ -183,7 +244,7 @@ var valueRef_1 = this_1["_" + field] || new ArraySchema_1.ArraySchema();

}
if (type_1.prototype instanceof Schema) {
if (type_2.prototype instanceof Schema) {
var item = void 0;
var isNew = (hasIndexChange && indexChangedFrom === undefined && newIndex !== undefined);
if (isNew) {
item = new type_1();
item = new type_2();
}

@@ -197,3 +258,3 @@ else if (indexChangedFrom !== undefined) {

if (!item) {
item = new type_1();
item = new type_2();
isNew = true;

@@ -216,3 +277,3 @@ }

else {
value[newIndex] = decodePrimitiveType(type_1, bytes, it);
value[newIndex] = decodePrimitiveType(type_2, bytes, it);
}

@@ -222,4 +283,4 @@ change.push(value[newIndex]);

}
else if (type_1.map) {
type_1 = type_1.map;
else if (type_2.map) {
type_2 = type_2.map;
var valueRef = this_1["_" + field] || new MapSchema_1.MapSchema();

@@ -254,3 +315,3 @@ value = valueRef.clone();

if (hasIndexChange && previousKey === undefined && hasMapIndex) {
item = new type_1();
item = new type_2();
}

@@ -263,4 +324,4 @@ else if (previousKey !== undefined) {

}
if (!item && type_1 !== "string") {
item = new type_1();
if (!item && type_2 !== "string") {
item = new type_2();
isNew = true;

@@ -279,4 +340,4 @@ }

}
else if (type_1 === "string") {
value[newKey] = decodePrimitiveType(type_1, bytes, it);
else if (type_2 === "string") {
value[newKey] = decodePrimitiveType(type_2, bytes, it);
}

@@ -297,3 +358,3 @@ else {

else {
value = decodePrimitiveType(type_1, bytes, it);
value = decodePrimitiveType(type_2, bytes, it);
hasChange = true;

@@ -321,8 +382,9 @@ }

};
Schema.prototype.encode = function (root, encodeAll) {
if (root === void 0) { root = true; }
Schema.prototype.encode = function (root, encodeAll, client) {
var _this = this;
if (root === void 0) { root = this; }
if (encodeAll === void 0) { encodeAll = false; }
var encodedBytes = [];
var endStructure = function () {
if (!root) {
if (_this !== root) {
encodedBytes.push(spec_1.END_OF_STRUCTURE);

@@ -338,2 +400,3 @@ }

var indexes = this._indexes;
var filters = this._filters;
var changes = (encodeAll)

@@ -344,4 +407,5 @@ ? this.$allChanges

var bytes = [];
var type_2 = schema[field];
var value = changes[field];
var type_3 = schema[field];
var filter_1 = (filters && filters[field]);
var value = (filter_1 && this.$allChanges[field]) || changes[field];
var fieldIndex = indexes[field];

@@ -352,14 +416,26 @@ // skip unchagned fields

}
if (type_2._schema) {
if (type_3._schema) {
if (client && filter_1) {
// skip if not allowed by custom filter
if (!filter_1.call(this, client, value, root)) {
continue;
}
}
encode.number(bytes, fieldIndex);
// encode child object
bytes = bytes.concat(value.encode(false, encodeAll));
// ensure parent is set
// in case it was manually instantiated
if (!value.$parent) {
value.$parent = this;
value.$parentField = field;
if (value) {
bytes = bytes.concat(value.encode(root, encodeAll, client));
// ensure parent is set
// in case it was manually instantiated
if (!value.$parent) {
value.$parent = this;
value.$parentField = field;
}
}
else {
// value has been removed
encode.uint8(bytes, spec_1.NIL);
}
}
else if (Array.isArray(type_2)) {
else if (Array.isArray(type_3)) {
encode.number(bytes, fieldIndex);

@@ -374,3 +450,9 @@ // total of items in the array

var item = this["_" + field][index];
if (typeof (type_2[0]) !== "string") { // is array of Schema
if (client && filter_1) {
// skip if not allowed by custom filter
if (!filter_1.call(this, client, item, root)) {
continue;
}
}
if (typeof (type_3[0]) !== "string") { // is array of Schema
encode.number(bytes, index);

@@ -390,7 +472,7 @@ if (item === undefined) {

}
bytes = bytes.concat(item.encode(false, encodeAll));
bytes = bytes.concat(item.encode(root, encodeAll, client));
}
else {
encode.number(bytes, i);
if (!encodePrimitiveType(type_2[0], bytes, index)) {
if (!encodePrimitiveType(type_3[0], bytes, index)) {
console.log("cannot encode", schema[field]);

@@ -402,3 +484,3 @@ continue;

}
else if (type_2.map) {
else if (type_3.map) {
// encode Map of type

@@ -413,2 +495,8 @@ encode.number(bytes, fieldIndex);

var mapItemIndex = this["_" + field]._indexes[key];
if (client && filter_1) {
// skip if not allowed by custom filter
if (!filter_1.call(this, client, item, root)) {
continue;
}
}
if (encodeAll) {

@@ -435,6 +523,6 @@ if (item) {

encode.string(bytes, key);
var mapKey = mapKeys.indexOf(key);
if (mapKey >= 0) {
this["_" + field]._indexes[key] = mapKey;
}
// const mapKey = mapKeys.indexOf(key);
// if (!client && mapKey >= 0) {
// this[`_${field}`]._indexes[key] = mapKey;
// }
}

@@ -444,6 +532,6 @@ if (item instanceof Schema) {

item.$parentField = [field, keys[i]];
bytes = bytes.concat(item.encode(false, encodeAll));
bytes = bytes.concat(item.encode(root, encodeAll, client));
}
else if (item !== undefined) {
encodePrimitiveType(type_2.map, bytes, item);
encodePrimitiveType(type_3.map, bytes, item);
}

@@ -454,7 +542,16 @@ else {

}
this["_" + field]._updateIndexes();
// TODO: track array/map indexes per client?
if (!client) {
this["_" + field]._updateIndexes();
}
}
else {
if (client && filter_1) {
// skip if not allowed by custom filter
if (!filter_1.call(this, client, value, root)) {
continue;
}
}
encode.number(bytes, fieldIndex);
if (!encodePrimitiveType(type_2, bytes, value)) {
if (!encodePrimitiveType(type_3, bytes, value)) {
console.log("cannot encode", schema[field]);

@@ -468,9 +565,17 @@ continue;

endStructure();
this.$changed = false;
this.$changes = {};
if (!client) {
this.$changed = false;
this.$changes = {};
}
return encodedBytes;
};
Schema.prototype.encodeFiltered = function (client) {
return this.encode(this, false, client);
};
Schema.prototype.encodeAll = function () {
return this.encode(true, true);
return this.encode(this, true);
};
Schema.prototype.encodeAllFiltered = function (client) {
return this.encode(this, true, client);
};
Schema.prototype.toJSON = function () {

@@ -608,3 +713,4 @@ var schema = this._schema;

});
var rootType = new schemaTypes[0];
var rootType = schemaTypes[0];
var rootInstance = new rootType();
/**

@@ -620,3 +726,3 @@ * auto-initialize referenced types on root type

var isMap = !isArray && fieldType.map;
rootType[fieldName] = (isArray)
rootInstance[fieldName] = (isArray)
? new ArraySchema_1.ArraySchema()

@@ -630,3 +736,3 @@ : (isMap)

}
return rootType;
return rootInstance;
};

@@ -640,3 +746,3 @@ __decorate([

/**
* Decorators / Proxies
* `@type()` decorator for proxies
*/

@@ -652,2 +758,3 @@ function type(type) {

constructor._indexes = {};
constructor._descriptors = {};
}

@@ -663,8 +770,8 @@ constructor._indexes[field] = Object.keys(constructor._schema).length;

var fieldCached = "_" + field;
Object.defineProperty(target, fieldCached, {
constructor._descriptors[fieldCached] = {
enumerable: false,
configurable: false,
writable: true,
});
Object.defineProperty(target, field, {
};
constructor._descriptors[field] = {
get: function () {

@@ -759,4 +866,7 @@ return this[fieldCached];

// directly assigning a `Schema` object
value.$parent = this;
value.$parentField = field;
// value may be set to null
if (value) {
value.$parent = this;
value.$parentField = field;
}
this.markAsChanged(field, value);

@@ -770,6 +880,22 @@ }

enumerable: true,
configurable: false
});
configurable: true
};
};
}
exports.type = type;
/**
* `@filter()` decorator for defining data filters per client
*/
function filter(cb) {
return function (target, field) {
var constructor = target.constructor;
/*
* static filters
*/
if (!constructor._filters) {
constructor._filters = {};
}
constructor._filters[field] = cb;
};
}
exports.filter = filter;
export { MapSchema } from "./types/MapSchema";
export { ArraySchema } from "./types/ArraySchema";
export { Schema, type, DataChange, PrimitiveType, Definition, DefinitionType, Reflection, ReflectionType, ReflectionField, } from "./annotations";
export { Schema, type, filter, DataChange, PrimitiveType, Definition, DefinitionType, FilterCallback, Reflection, ReflectionType, ReflectionField, } from "./annotations";

@@ -11,2 +11,3 @@ "use strict";

exports.type = annotations_1.type;
exports.filter = annotations_1.filter;
// Reflection

@@ -13,0 +14,0 @@ exports.Reflection = annotations_1.Reflection;

@@ -23,3 +23,2 @@ "use strict";

currentClass = new Class();
;
classes.push(currentClass);

@@ -30,2 +29,6 @@ break;

case ts.SyntaxKind.Identifier:
// console.log("NODE =>", node.getText());
if (node.getText() === "type" && node.parent.kind !== ts.SyntaxKind.ImportSpecifier) {
console.log("TYPE DECORATORS:", node.parent.parent.parent.decorators[0].getText());
}
if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) {

@@ -32,0 +35,0 @@ currentClass.name = node.getText();

{
"name": "@colyseus/schema",
"version": "0.3.2",
"version": "0.3.3",
"description": "Schema-based binary serializer / de-serializer. ",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"bin": {
"stateify": "./bin/stateify"
},
"scripts": {
"test": "mocha --require ts-node/register test/**Test.ts",
"watch": "tsc -w",
"prepublish": "tsc"

@@ -10,0 +14,0 @@ },

@@ -19,15 +19,2 @@ <div align="center">

> WORK-IN-PROGRESS EXPERIMENT OF A NEW SERIALIZATION ALGORITHM FOR [COLYSEUS](https://github.com/gamestdio/colyseus)
Initial thoghts/assumptions:
- no bottleneck to detect state changes.
- have a schema definition on both server and client
- better experience on staticaly-typed languages (C#, C++)
- mutations should be cheap.
Practical Colyseus issues this should solve:
- Avoid decoding large objects that haven't been patched
- Allow to send different patches for each client
- Better developer experience on statically-typed languages
## Defining Schema

@@ -110,4 +97,11 @@

#### Array of a primitive type
#### Array of custom `Schema` type
```typescript
@type([ Player ])
arrayOfPlayers: ArraySchema<Player>;
```
#### Array of a primitive type (**not currently supported!**)
You can't mix types inside arrays.

@@ -123,10 +117,10 @@

#### Array of custom `Schema` type
#### Map of custom `Schema` type
```typescript
@type([ Player ])
arrayOfPlayers: ArraySchema<Player>;
@type({ map: Player })
mapOfPlayers: MapSchema<Player>;
```
#### Map of a primitive type
#### Map of a primitive type (**not currently supported!**)

@@ -143,7 +137,24 @@ You can't mix types inside maps.

#### Map of custom `Schema` type
### Data filters (experimental)
When using with [Colyseus 0.10](https://github.com/colyseus/colyseus), you may provide a `@filter` per field, to filter out what you don't want to serialize for a specific client.
On the example below, we are filtering entities which are close to the player entity.
```typescript
@type({ map: Player })
mapOfPlayers: MapSchema<Player>;
import { Schema, type, filter } from "@colyseus/schema";
export class State extends Schema {
@filter(function(this: State, client: any, value: Entity) {
const currentPlayer = this.entities[client.sessionId]
var a = value.x - currentPlayer.x;
var b = value.y - currentPlayer.y;
return (Math.sqrt(a * a + b * b)) <= 10;
})
@type({ map: Entity })
entities = new MapSchema<Entity>();
}
```

@@ -203,51 +214,2 @@

## Aimed usage on Colyseus
This is the ideal scenario that should be possible to achieve.
### Customizing which data each client will receive
```typescript
class MyRoom extends Room<State> {
onInit() {
this.setState(new State());
}
onPatch (client: Client, state: State) {
const player = state.players[client.sessionId];
// filter enemies closer to current player
state.enemies = state.enemies.filter(enemy =>
distance(enemy.x, enemy.y, player.x, player.y) < 50);
return state;
}
}
```
### Broadcasting different patches for each client
```typescript
class Room<T> {
// ...
public onPatch?(client: Client, state: T);
// ...
broadcastPatch() {
if (this.onPatch) {
for (let i=0; i<this.clients.length; i++) {
const client = this.clients[i];
const filteredState = this.onPatch(client, this.state.clone());
send(client, filteredState.encode());
}
} else {
this.broadcast(this.state.encode());
}
}
}
```
## Benchmarks:

@@ -263,2 +225,15 @@

## Why
Initial thoghts/assumptions, for Colyseus:
- little to no bottleneck for detecting state changes.
- have a schema definition on both server and client
- better experience on staticaly-typed languages (C#, C++)
- mutations should be cheap.
Practical Colyseus issues this should solve:
- Avoid decoding large objects that haven't been patched
- Allow to send different patches for each client
- Better developer experience on statically-typed languages
## Inspiration:

@@ -265,0 +240,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc