refactor: schema

This commit is contained in:
cha0s 2024-07-23 10:10:32 -05:00
parent 263cf37e27
commit 88f0ec4715
17 changed files with 396 additions and 294 deletions

View File

@ -38,7 +38,8 @@ export default class Component {
return []; return [];
} }
const allocated = this.allocateMany(entries.length); const allocated = this.allocateMany(entries.length);
const {properties} = this.constructor.schema.specification; const {properties} = this.constructor.schema.specification.concrete;
const Schema = this.constructor.schema.constructor;
const keys = Object.keys(properties); const keys = Object.keys(properties);
const promises = []; const promises = [];
for (let i = 0; i < entries.length; ++i) { for (let i = 0; i < entries.length; ++i) {
@ -47,13 +48,15 @@ export default class Component {
this.data[allocated[i]].entity = entityId; this.data[allocated[i]].entity = entityId;
for (let k = 0; k < keys.length; ++k) { for (let k = 0; k < keys.length; ++k) {
const j = keys[k]; const j = keys[k];
const {defaultValue} = properties[j];
const instance = this.data[allocated[i]]; const instance = this.data[allocated[i]];
if (j in values) { if (j in values) {
instance[j] = values[j]; instance[j] = values[j];
} }
else if ('undefined' !== typeof defaultValue) { else {
instance[j] = defaultValue; const defaultValue = Schema.defaultValue(properties[j]);
if ('undefined' !== typeof defaultValue) {
instance[j] = defaultValue;
}
} }
} }
promises.push(this.load(this.data[allocated[i]])); promises.push(this.load(this.data[allocated[i]]));
@ -67,7 +70,7 @@ export default class Component {
} }
deserialize(entityId, view, offset) { deserialize(entityId, view, offset) {
const {properties} = this.constructor.schema.specification; const {properties} = this.constructor.schema.specification.concrete;
const instance = this.get(entityId); const instance = this.get(entityId);
const deserialized = this.constructor.schema.deserialize(view, offset); const deserialized = this.constructor.schema.deserialize(view, offset);
for (const key in properties) { for (const key in properties) {
@ -96,11 +99,11 @@ export default class Component {
} }
static filterDefaults(instance) { static filterDefaults(instance) {
const {properties} = this.schema.specification; const {properties} = this.schema.specification.concrete;
const Schema = this.schema.constructor;
const json = {}; const json = {};
for (const key in properties) { for (const key in properties) {
const {defaultValue} = properties[key]; if (key in instance && instance[key] !== Schema.defaultValue(properties[key])) {
if (key in instance && instance[key] !== defaultValue) {
json[key] = instance[key]; json[key] = instance[key];
} }
} }
@ -136,16 +139,16 @@ export default class Component {
instanceFromSchema() { instanceFromSchema() {
const Component = this; const Component = this;
const {specification} = Component.constructor.schema; const {concrete} = Component.constructor.schema.specification;
const Schema = Component.constructor.schema.constructor;
const Instance = class { const Instance = class {
$$entity = 0; $$entity = 0;
constructor() { constructor() {
this.$$reset(); this.$$reset();
} }
$$reset() { $$reset() {
for (const key in specification.properties) { for (const key in concrete.properties) {
const {defaultValue} = specification.properties[key]; this[`$$${key}`] = Schema.defaultValue(concrete.properties[key]);
this[`$$${key}`] = defaultValue;
} }
} }
destroy() {} destroy() {}
@ -166,7 +169,7 @@ export default class Component {
this.$$reset(); this.$$reset();
}, },
}; };
for (const key in specification.properties) { for (const key in concrete.properties) {
properties[key] = { properties[key] = {
get: function get() { get: function get() {
return this[`$$${key}`]; return this[`$$${key}`];

View File

@ -0,0 +1,40 @@
export default function (Schema) {
return {
defaultValue: () => [],
deserialize: (view, offset, {subtype}) => {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const value = [];
for (let i = 0; i < length; ++i) {
value.push(Schema.deserialize(view, offset, subtype));
}
return value;
},
normalize: ({subtype}) => {
return {
subtype: Schema.normalize(subtype),
}
},
serialize: (source, view, offset, {subtype}) => {
view.setUint32(offset, source.length, true);
let size = 4;
for (const element of source) {
size += Schema.serialize(
element,
view,
offset + size,
subtype,
);
}
return size;
},
sizeOf: (instance, {subtype}) => {
let size = 4;
for (const element of instance) {
size += Schema.sizeOf(element, subtype);
}
return size;
},
staticSizeOf: () => 0,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getFloat32(offset.value, true);
offset.value += 4;
return value;
},
serialize: (source, view, offset) => {
view.setFloat32(offset, source, true);
return 4;
},
sizeOf: () => 4,
staticSizeOf: () => 4,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getFloat64(offset.value, true);
offset.value += 8;
return value;
},
serialize: (source, view, offset) => {
view.setFloat64(offset, source, true);
return 8;
},
sizeOf: () => 8,
staticSizeOf: () => 8,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getInt16(offset.value, true);
offset.value += 2;
return value;
},
serialize: (source, view, offset) => {
view.setInt16(offset, source, true);
return 2;
},
sizeOf: () => 2,
staticSizeOf: () => 2,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getInt32(offset.value, true);
offset.value += 4;
return value;
},
serialize: (source, view, offset) => {
view.setInt32(offset, source, true);
return 4;
},
sizeOf: () => 4,
staticSizeOf: () => 4,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0n,
deserialize: (view, offset) => {
const value = view.getBigInt64(offset.value, true);
offset.value += 8;
return value;
},
serialize: (source, view, offset) => {
view.setBigInt64(offset, source, true);
return 8;
},
sizeOf: () => 8,
staticSizeOf: () => 8,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getInt8(offset.value, true);
offset.value += 1;
return value;
},
serialize: (source, view, offset) => {
view.setInt8(offset, source, true);
return 1;
},
sizeOf: () => 1,
staticSizeOf: () => 1,
};
}

View File

@ -0,0 +1,50 @@
export default function (Schema) {
return {
defaultValue: () => ({}),
deserialize: (view, offset, concrete) => {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const value = {};
for (let i = 0; i < length; ++i) {
const key = Schema.deserialize(view, offset, concrete.key);
value[key] = Schema.deserialize(view, offset, concrete.value);
}
return value;
},
normalize: ({value}) => {
return {
key: Schema.normalize({type: 'string'}),
value: Schema.normalize(value),
}
},
serialize: (source, view, offset, concrete) => {
const keys = Object.keys(source);
view.setUint32(offset, keys.length, true);
let size = 4;
for (const key of keys) {
size += Schema.serialize(
key,
view,
offset + size,
concrete.key,
);
size += Schema.serialize(
source[key],
view,
offset + size,
concrete.value,
);
}
return size;
},
sizeOf: (instance, concrete) => {
let size = 4;
for (const key in instance) {
size += Schema.sizeOf(key, concrete.key);
size += Schema.sizeOf(instance[key], concrete.value);
}
return size;
},
staticSizeOf: () => 0,
};
}

View File

@ -0,0 +1,55 @@
export default function (Schema) {
return {
defaultValue: ({properties}) => {
const object = {};
for (const key in properties) {
object[key] = Schema.defaultValue(properties[key]);
}
return object;
},
deserialize: (view, offset, {properties}) => {
const value = {};
for (const key in properties) {
value[key] = Schema.deserialize(view, offset, properties[key]);
}
return value;
},
normalize: ({properties}) => {
const normalized = {properties: {}};
for (const key in properties) {
normalized.properties[key] = Schema.normalize(properties[key])
}
return normalized;
},
serialize: (source, view, offset, {properties}) => {
let size = 0;
for (const key in properties) {
size += Schema.serialize(
source[key],
view,
offset + size,
properties[key],
);
}
return size;
},
sizeOf: (instance, {properties}) => {
let size = 0;
for (const key in properties) {
size += Schema.sizeOf(instance[key], properties[key]);
}
return size;
},
staticSizeOf: ({properties}) => {
let size = 0;
for (const key in properties) {
const propertySize = Schema.size(properties[key]);
if (0 === propertySize) {
return 0;
}
size += propertySize;
}
return size;
},
};
}

View File

@ -0,0 +1,31 @@
const decoder = new TextDecoder();
const encoder = new TextEncoder();
export default function () {
return {
defaultValue: () => '',
deserialize: (view, offset) => {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const {buffer, byteOffset} = view;
const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length));
offset.value += length;
return value;
},
serialize: (source, view, offset) => {
const bytes = encoder.encode(source);
view.setUint32(offset, bytes.length, true);
offset += 4;
for (let i = 0; i < bytes.length; ++i) {
view.setUint8(offset++, bytes[i]);
}
return 4 + bytes.length;
},
sizeOf: (instance) => {
let size = 4;
size += (encoder.encode(instance)).length;
return size;
},
staticSizeOf: () => 0,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getUint16(offset.value, true);
offset.value += 2;
return value;
},
serialize: (source, view, offset) => {
view.setUint16(offset, source, true);
return 2;
},
sizeOf: () => 2,
staticSizeOf: () => 2,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getUint32(offset.value, true);
offset.value += 4;
return value;
},
serialize: (source, view, offset) => {
view.setUint32(offset, source, true);
return 4;
},
sizeOf: () => 4,
staticSizeOf: () => 4,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0n,
deserialize: (view, offset) => {
const value = view.getBigUint64(offset.value, true);
offset.value += 8;
return value;
},
serialize: (source, view, offset) => {
view.setBigUint64(offset, source, true);
return 8;
},
sizeOf: () => 8,
staticSizeOf: () => 8,
};
}

View File

@ -0,0 +1,16 @@
export default function () {
return {
defaultValue: () => 0,
deserialize: (view, offset) => {
const value = view.getUint8(offset.value, true);
offset.value += 1;
return value;
},
serialize: (source, view, offset) => {
view.setUint8(offset, source, true);
return 1;
},
sizeOf: () => 1,
staticSizeOf: () => 1,
};
}

View File

@ -1,303 +1,66 @@
const encoder = new TextEncoder();
export default class Schema { export default class Schema {
$$size = 0; static $$types = {};
specification; specification;
static viewGetMethods = {
uint8: 'getUint8',
int8: 'getInt8',
uint16: 'getUint16',
int16: 'getInt16',
uint32: 'getUint32',
int32: 'getInt32',
float32: 'getFloat32',
float64: 'getFloat64',
int64: 'getBigInt64',
uint64: 'getBigUint64',
};
static viewSetMethods = {
uint8: 'setUint8',
int8: 'setInt8',
uint16: 'setUint16',
int16: 'setInt16',
uint32: 'setUint32',
int32: 'setInt32',
float32: 'setFloat32',
float64: 'setFloat64',
int64: 'setBigInt64',
uint64: 'setBigUint64',
};
constructor(specification) { constructor(specification) {
this.specification = this.constructor.normalize(specification); this.specification = this.constructor.normalize(specification);
} }
static deserialize(view, offset, specification) { static defaultValue({$, concrete}) {
const viewGetMethod = this.viewGetMethods[specification.type]; if (concrete.defaultValue) {
if (viewGetMethod) { return concrete.defaultValue;
const value = view[viewGetMethod](offset.value, true);
offset.value += this.size(specification);
return value;
}
switch (specification.type) {
case 'array': {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const value = [];
for (let i = 0; i < length; ++i) {
value.push(this.deserialize(view, offset, specification.subtype));
}
return value;
}
case 'map': {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const value = {};
for (let i = 0; i < length; ++i) {
const key = this.deserialize(view, offset, {type: 'string'});
value[key] = this.deserialize(view, offset, specification.value);
}
return value;
}
case 'object': {
const value = {};
for (const key in specification.properties) {
value[key] = this.deserialize(view, offset, specification.properties[key]);
}
return value;
}
case 'string': {
const length = view.getUint32(offset.value, true);
offset.value += 4;
const {buffer, byteOffset} = view;
const decoder = new TextDecoder();
const value = decoder.decode(new DataView(buffer, byteOffset + offset.value, length));
offset.value += length;
return value;
}
}
}
deserialize(view, offset = 0) {
const wrapped = {value: offset};
return this.constructor.deserialize(view, wrapped, this.specification);
}
static defaultValue(specification) {
if (specification.defaultValue) {
return specification.defaultValue;
}
switch (specification.type) {
case 'uint8': case 'int8':
case 'uint16': case 'int16':
case 'uint32': case 'int32':
case 'uint64': case 'int64':
case 'float32': case 'float64': {
return 0;
}
case 'array': {
return [];
}
case 'map': {
return {};
}
case 'object': {
const object = {};
for (const key in specification.properties) {
object[key] = this.defaultValue(specification.properties[key]);
}
return object;
}
case 'string': {
return '';
}
} }
return $.defaultValue(concrete);
} }
defaultValue() { defaultValue() {
return this.constructor.defaultValue(this.specification); return this.constructor.defaultValue(this.specification);
} }
static normalize(specification) { static deserialize(view, offset, {$, concrete}) {
let normalized = specification; return $.deserialize(view, offset, concrete);
switch (specification.type) {
case 'array': {
normalized.subtype = this.normalize(specification.subtype);
break;
}
case 'map': {
normalized.value = this.normalize(specification.value);
break;
}
case 'object': {
for (const key in specification.properties) {
normalized.properties[key] = this.normalize(specification.properties[key])
}
break;
}
case 'uint8':
case 'int8':
case 'uint16':
case 'int16':
case 'uint32':
case 'int32':
case 'uint64':
case 'int64':
case 'float32':
case 'float64':
case 'string': {
break;
}
/* v8 ignore next 2 */
default:
throw new TypeError(`invalid specification: ${JSON.stringify(specification)}`);
}
return {...normalized, defaultValue: this.defaultValue(normalized)};
} }
static serialize(source, view, offset, specification) { deserialize(view, offset = 0) {
const viewSetMethod = this.viewSetMethods[specification.type]; return this.constructor.deserialize(view, {value: offset}, this.specification);
if (viewSetMethod) { }
view[viewSetMethod](offset, source, true);
return this.size(specification); static normalize({type, ...rest}) {
} const $$type = this.$$types[type];
switch (specification.type) { if (!$$type) {
case 'array': { throw new TypeError(`unregistered schema type '${type}'`);
view.setUint32(offset, source.length, true);
let arraySize = 4;
for (const element of source) {
arraySize += this.serialize(
element,
view,
offset + arraySize,
specification.subtype,
);
}
return arraySize;
}
case 'map': {
const keys = Object.keys(source);
view.setUint32(offset, keys.length, true);
let mapSize = 4;
for (const key of keys) {
mapSize += this.serialize(
key,
view,
offset + mapSize,
{type: 'string'},
);
mapSize += this.serialize(
source[key],
view,
offset + mapSize,
specification.value,
);
}
return mapSize;
}
case 'object': {
let objectSize = 0;
for (const key in specification.properties) {
objectSize += this.serialize(
source[key],
view,
offset + objectSize,
specification.properties[key],
);
}
return objectSize;
}
case 'string': {
const encoder = new TextEncoder();
const bytes = encoder.encode(source);
view.setUint32(offset, bytes.length, true);
offset += 4;
for (let i = 0; i < bytes.length; ++i) {
view.setUint8(offset++, bytes[i]);
}
return 4 + bytes.length;
}
} }
return {
$: this.$$types[type],
concrete: $$type.normalize ? $$type.normalize(rest) : rest,
};
}
static serialize(source, view, offset, {$, concrete}) {
return $.serialize(source, view, offset, concrete);
} }
serialize(source, view, offset = 0) { serialize(source, view, offset = 0) {
this.constructor.serialize(source, view, offset, this.specification); this.constructor.serialize(source, view, offset, this.specification);
} }
static sizeOf(concrete, specification) { static sizeOf(instance, {$, concrete}) {
const size = this.size(specification); return $.sizeOf(instance, concrete);
if (size > 0) {
return size;
}
let fullSize = 0;
const {type} = specification;
switch (type) {
case 'array': {
fullSize += 4;
for (const element of concrete) {
fullSize += this.sizeOf(element, specification.subtype);
}
break;
}
case 'map': {
fullSize += 4;
for (const key in concrete) {
fullSize += this.sizeOf(key, {type: 'string'});
fullSize += this.sizeOf(concrete[key], specification.value);
}
break;
}
case 'object': {
for (const key in specification.properties) {
fullSize += this.sizeOf(concrete[key], specification.properties[key]);
}
break;
}
case 'string':
fullSize += 4;
fullSize += (encoder.encode(concrete)).length;
break;
}
return fullSize;
} }
sizeOf(concrete) { sizeOf(instance) {
return this.constructor.sizeOf(concrete, this.specification); return this.constructor.sizeOf(instance, this.specification);
} }
static size(specification) { static size({$, concrete}) {
switch (specification.type) { return $.staticSizeOf(concrete);
case 'array': return 0;
case 'map': return 0; // TODO could be fixed-size w/ fixed-size key
case 'object': {
let size = 0;
for (const key in specification.properties) {
const propertySize = this.size(specification.properties[key]);
if (0 === propertySize) {
return 0;
}
size += propertySize;
}
return size;
}
case 'uint8': case 'int8': {
return 1;
}
case 'uint16': case 'int16': {
return 2;
}
case 'uint32': case 'int32': case 'float32': {
return 4;
}
case 'uint64': case 'int64': case 'float64': {
return 8;
}
default: return 0;
}
} }
} }
const imports = import.meta.glob('./schema-types/*.js', {eager: true, import: 'default'});
for (const path in imports) {
Schema.$$types[path.replace(/.\/schema-types\/(.*)\.js/, '$1')] = imports[path](Schema);
}

View File

@ -4,10 +4,6 @@ import Schema from './schema.js';
test('defaults values', () => { test('defaults values', () => {
const compare = (specification, value) => { const compare = (specification, value) => {
expect(Schema.defaultValue(specification))
.to.deep.equal(value);
expect(new Schema(specification).specification.defaultValue)
.to.deep.equal(value);
expect(new Schema(specification).defaultValue()) expect(new Schema(specification).defaultValue())
.to.deep.equal(value); .to.deep.equal(value);
}; };
@ -18,13 +14,17 @@ test('defaults values', () => {
'int16', 'int16',
'uint32', 'uint32',
'int32', 'int32',
'uint64',
'int64',
'float32', 'float32',
'float64', 'float64',
].forEach((type) => { ].forEach((type) => {
compare({type}, 0); compare({type}, 0);
}); });
[
'uint64',
'int64',
].forEach((type) => {
compare({type}, 0n);
});
compare({type: 'string'}, ''); compare({type: 'string'}, '');
compare({type: 'array', subtype: {type: 'string'}}, []); compare({type: 'array', subtype: {type: 'string'}}, []);
compare( compare(
@ -167,11 +167,11 @@ test('encodes and decodes', () => {
[{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}], [{type: 'object', properties: {foo: {type: 'uint8'}, bar: {type: 'string'}}}, {foo: 64, bar: 'baz'}],
[{type: 'object', properties: {foo: {type: 'uint8'}}}, {foo: 64}], [{type: 'object', properties: {foo: {type: 'uint8'}}}, {foo: 64}],
]; ];
entries.forEach(([specification, concrete]) => { entries.forEach(([specification, instance]) => {
const schema = new Schema(specification); const schema = new Schema(specification);
const view = new DataView(new ArrayBuffer(schema.sizeOf(concrete))); const view = new DataView(new ArrayBuffer(schema.sizeOf(instance)));
schema.serialize(concrete, view); schema.serialize(instance, view);
expect(concrete) expect(instance)
.to.deep.equal(schema.deserialize(view)); .to.deep.equal(schema.deserialize(view));
}); });
}); });