@dcl/ecs
Version:
Decentraland ECS
281 lines (279 loc) • 11.1 kB
JavaScript
import { ReadWriteByteBuffer } from '../serialization/ByteBuffer';
import { ProcessMessageResultType, CrdtMessageType, PutComponentOperation, DeleteComponent } from '../serialization/crdt';
import { dataCompare } from '../systems/crdt/utils';
import { deepReadonly } from './readonly';
export function incrementTimestamp(entity, timestamps) {
const newTimestamp = (timestamps.get(entity) || 0) + 1;
timestamps.set(entity, newTimestamp);
return newTimestamp;
}
export function createDumpLwwFunctionFromCrdt(componentId, timestamps, schema, data) {
return function dumpCrdtState(buffer, filterEntity) {
for (const [entity, timestamp] of timestamps) {
/* istanbul ignore if */
if (filterEntity) {
// I swear that this is being tested on state-to-crdt.spec but jest is trolling me
/* istanbul ignore next */
if (!filterEntity(entity))
continue;
}
/* istanbul ignore else */
if (data.has(entity)) {
const it = data.get(entity);
const buf = new ReadWriteByteBuffer();
schema.serialize(it, buf);
PutComponentOperation.write(entity, timestamp, componentId, buf.toBinary(), buffer);
}
else {
DeleteComponent.write(entity, componentId, timestamp, buffer);
}
}
};
}
export function createUpdateLwwFromCrdt(componentId, timestamps, schema, data) {
/**
* Process the received message only if the lamport number recieved is higher
* than the stored one. If its lower, we spread it to the network to correct the peer.
* If they are equal, the bigger raw data wins.
* Returns the recieved data if the lamport number was bigger than ours.
* If it was an outdated message, then we return void
* @public
*/
function crdtRuleForCurrentState(message) {
const { entityId, timestamp } = message;
const currentTimestamp = timestamps.get(entityId);
// The received message is > than our current value, update our state.components.
if (currentTimestamp === undefined || currentTimestamp < timestamp) {
return ProcessMessageResultType.StateUpdatedTimestamp;
}
// Outdated Message. Resend our state message through the wire.
if (currentTimestamp > timestamp) {
// console.log('2', currentTimestamp, timestamp)
return ProcessMessageResultType.StateOutdatedTimestamp;
}
// Deletes are idempotent
if (message.type === CrdtMessageType.DELETE_COMPONENT && !data.has(entityId)) {
return ProcessMessageResultType.NoChanges;
}
let currentDataGreater = 0;
if (data.has(entityId)) {
const writeBuffer = new ReadWriteByteBuffer();
schema.serialize(data.get(entityId), writeBuffer);
currentDataGreater = dataCompare(writeBuffer.toBinary(), message.data || null);
}
else {
currentDataGreater = dataCompare(null, message.data);
}
// Same data, same timestamp. Weirdo echo message.
// console.log('3', currentDataGreater, writeBuffer.toBinary(), (message as any).data || null)
if (currentDataGreater === 0) {
return ProcessMessageResultType.NoChanges;
}
else if (currentDataGreater > 0) {
// Current data is greater
return ProcessMessageResultType.StateOutdatedData;
}
else {
// Curent data is lower
return ProcessMessageResultType.StateUpdatedData;
}
}
return (msg) => {
/* istanbul ignore next */
if (msg.type !== CrdtMessageType.PUT_COMPONENT &&
msg.type !== CrdtMessageType.PUT_COMPONENT_NETWORK &&
msg.type !== CrdtMessageType.DELETE_COMPONENT &&
msg.type !== CrdtMessageType.DELETE_COMPONENT_NETWORK)
/* istanbul ignore next */
return [null, data.get(msg.entityId)];
const action = crdtRuleForCurrentState(msg);
const entity = msg.entityId;
switch (action) {
case ProcessMessageResultType.StateUpdatedData:
case ProcessMessageResultType.StateUpdatedTimestamp: {
timestamps.set(entity, msg.timestamp);
if (msg.type === CrdtMessageType.PUT_COMPONENT || msg.type === CrdtMessageType.PUT_COMPONENT_NETWORK) {
const buf = new ReadWriteByteBuffer(msg.data);
data.set(entity, schema.deserialize(buf));
}
else {
data.delete(entity);
}
return [null, data.get(entity)];
}
case ProcessMessageResultType.StateOutdatedTimestamp:
case ProcessMessageResultType.StateOutdatedData: {
if (data.has(entity)) {
const writeBuffer = new ReadWriteByteBuffer();
schema.serialize(data.get(entity), writeBuffer);
return [
{
type: CrdtMessageType.PUT_COMPONENT,
componentId,
data: writeBuffer.toBinary(),
entityId: entity,
timestamp: timestamps.get(entity)
},
data.get(entity)
];
}
else {
return [
{
type: CrdtMessageType.DELETE_COMPONENT,
componentId,
entityId: entity,
timestamp: timestamps.get(entity)
},
undefined
];
}
}
}
return [null, data.get(entity)];
};
}
export function createGetCrdtMessagesForLww(componentId, timestamps, dirtyIterator, schema, data) {
return function* () {
for (const entity of dirtyIterator) {
const newTimestamp = incrementTimestamp(entity, timestamps);
if (data.has(entity)) {
const writeBuffer = new ReadWriteByteBuffer();
schema.serialize(data.get(entity), writeBuffer);
const msg = {
type: CrdtMessageType.PUT_COMPONENT,
componentId,
entityId: entity,
data: writeBuffer.toBinary(),
timestamp: newTimestamp
};
yield msg;
}
else {
const msg = {
type: CrdtMessageType.DELETE_COMPONENT,
componentId,
entityId: entity,
timestamp: newTimestamp
};
yield msg;
}
}
dirtyIterator.clear();
};
}
/**
* @internal
*/
export function createComponentDefinitionFromSchema(componentName, componentId, schema) {
const data = new Map();
const dirtyIterator = new Set();
const timestamps = new Map();
const onChangeCallbacks = new Map();
return {
get componentId() {
return componentId;
},
get componentName() {
return componentName;
},
get componentType() {
// a getter is used here to prevent accidental changes
return 0 /* ComponentType.LastWriteWinElementSet */;
},
schema,
has(entity) {
return data.has(entity);
},
deleteFrom(entity, markAsDirty = true) {
const component = data.get(entity);
if (data.delete(entity) && markAsDirty) {
dirtyIterator.add(entity);
}
return component || null;
},
entityDeleted(entity, markAsDirty) {
if (data.delete(entity) && markAsDirty) {
dirtyIterator.add(entity);
}
},
getOrNull(entity) {
const component = data.get(entity);
return component ? deepReadonly(component) : null;
},
get(entity) {
const component = data.get(entity);
if (!component) {
throw new Error(`[getFrom] Component ${componentName} for entity #${entity} not found`);
}
return deepReadonly(component);
},
create(entity, value) {
const component = data.get(entity);
if (component) {
throw new Error(`[create] Component ${componentName} for ${entity} already exists`);
}
const usedValue = value === undefined ? schema.create() : schema.extend ? schema.extend(value) : value;
data.set(entity, usedValue);
dirtyIterator.add(entity);
return usedValue;
},
createOrReplace(entity, value) {
const usedValue = value === undefined ? schema.create() : schema.extend ? schema.extend(value) : value;
data.set(entity, usedValue);
dirtyIterator.add(entity);
return usedValue;
},
getMutableOrNull(entity) {
const component = data.get(entity);
if (!component) {
return null;
}
dirtyIterator.add(entity);
return component;
},
getOrCreateMutable(entity, value) {
const component = data.get(entity);
if (!component) {
return this.create(entity, value);
}
else {
dirtyIterator.add(entity);
return component;
}
},
getMutable(entity) {
const component = this.getMutableOrNull(entity);
if (component === null) {
throw new Error(`[mutable] Component ${componentName} for ${entity} not found`);
}
return component;
},
*iterator() {
for (const [entity, component] of data) {
yield [entity, component];
}
},
*dirtyIterator() {
for (const entity of dirtyIterator) {
yield entity;
}
},
getCrdtUpdates: createGetCrdtMessagesForLww(componentId, timestamps, dirtyIterator, schema, data),
updateFromCrdt: createUpdateLwwFromCrdt(componentId, timestamps, schema, data),
dumpCrdtStateToBuffer: createDumpLwwFunctionFromCrdt(componentId, timestamps, schema, data),
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);
}
}
};
}