@dcl/ecs
Version:
Decentraland ECS
159 lines (158 loc) • 5.3 kB
JavaScript
import { ReadWriteByteBuffer } from '../serialization/ByteBuffer';
import { AppendValueOperation, CrdtMessageType } from '../serialization/crdt';
import { __DEV__ } from '../runtime/invariant';
const emptyReadonlySet = freezeSet(new Set());
function frozenError() {
throw new Error('The set is frozen');
}
function freezeSet(set) {
;
set.add = frozenError;
set.clear = frozenError;
return set;
}
function sortByTimestamp(a, b) {
return a.timestamp > b.timestamp ? 1 : -1;
}
/**
* @internal
*/
export function createValueSetComponentDefinitionFromSchema(componentName, componentId, schema, options) {
const data = new Map();
const dirtyIterator = new Set();
const queuedCommands = [];
const onChangeCallbacks = new Map();
// only sort the array if the latest (N) element has a timestamp <= N-1
function shouldSort(row) {
const len = row.raw.length;
if (len > 1 && row.raw[len - 1].timestamp <= row.raw[len - 2].timestamp) {
return true;
}
return false;
}
function gotUpdated(entity) {
const row = data.get(entity);
/* istanbul ignore else */
if (row) {
if (shouldSort(row)) {
row.raw.sort(sortByTimestamp);
}
while (row.raw.length > options.maxElements) {
row.raw.shift();
}
const frozenSet = freezeSet(new Set(row?.raw.map(($) => $.value)));
row.frozenSet = frozenSet;
return frozenSet;
}
else {
/* istanbul ignore next */
return emptyReadonlySet;
}
}
function append(entity, value) {
let row = data.get(entity);
if (!row) {
row = { raw: [], frozenSet: emptyReadonlySet };
data.set(entity, row);
}
const usedValue = schema.extend ? schema.extend(value) : value;
const timestamp = options.timestampFunction(usedValue);
if (__DEV__) {
// only freeze the objects in dev mode to warn the developers because
// it is an expensive operation
Object.freeze(usedValue);
}
row.raw.push({ value: usedValue, timestamp });
return { set: gotUpdated(entity), value: usedValue };
}
const ret = {
get componentId() {
return componentId;
},
get componentName() {
return componentName;
},
get componentType() {
// a getter is used here to prevent accidental changes
return 1 /* ComponentType.GrowOnlyValueSet */;
},
schema,
has(entity) {
return data.has(entity);
},
entityDeleted(entity) {
data.delete(entity);
},
get(entity) {
const values = data.get(entity);
if (values) {
return values.frozenSet;
}
else {
return emptyReadonlySet;
}
},
addValue(entity, rawValue) {
const { set, value } = append(entity, rawValue);
dirtyIterator.add(entity);
const buf = new ReadWriteByteBuffer();
schema.serialize(value, buf);
queuedCommands.push({
componentId,
data: buf.toBinary(),
entityId: entity,
timestamp: 0,
type: CrdtMessageType.APPEND_VALUE
});
return set;
},
*iterator() {
for (const [entity, component] of data) {
yield [entity, component.frozenSet];
}
},
*dirtyIterator() {
for (const entity of dirtyIterator) {
yield entity;
}
},
getCrdtUpdates() {
// return a copy of the commands, and then clear the local copy
dirtyIterator.clear();
return queuedCommands.splice(0, queuedCommands.length);
},
updateFromCrdt(_body) {
if (_body.type === CrdtMessageType.APPEND_VALUE) {
const buf = new ReadWriteByteBuffer(_body.data);
const { value } = append(_body.entityId, schema.deserialize(buf));
return [null, value];
}
return [null, undefined];
},
dumpCrdtStateToBuffer: function (buffer, filterEntity) {
for (const [entity, { raw }] of data) {
if (filterEntity && !filterEntity(entity))
continue;
for (const it of raw) {
const buf = new ReadWriteByteBuffer();
schema.serialize(it.value, buf);
AppendValueOperation.write(entity, 0, componentId, buf.toBinary(), buffer);
}
}
},
onChange(entity, cb) {
const cbs = onChangeCallbacks.get(entity) ?? [];
cbs.push(cb);
onChangeCallbacks.set(entity, cbs);
},
__onChangeCallbacks(entity, value) {
const cbs = onChangeCallbacks.get(entity);
if (!cbs)
return;
for (const cb of cbs) {
cb(value);
}
}
};
return ret;
}