@dcl/ecs
Version:
Decentraland ECS
289 lines (287 loc) • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createComponentDefinitionFromSchema = exports.createGetCrdtMessagesForLww = exports.createUpdateLwwFromCrdt = exports.createDumpLwwFunctionFromCrdt = exports.incrementTimestamp = void 0;
const ByteBuffer_1 = require("../serialization/ByteBuffer");
const crdt_1 = require("../serialization/crdt");
const utils_1 = require("../systems/crdt/utils");
const readonly_1 = require("./readonly");
function incrementTimestamp(entity, timestamps) {
const newTimestamp = (timestamps.get(entity) || 0) + 1;
timestamps.set(entity, newTimestamp);
return newTimestamp;
}
exports.incrementTimestamp = incrementTimestamp;
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 ByteBuffer_1.ReadWriteByteBuffer();
schema.serialize(it, buf);
crdt_1.PutComponentOperation.write(entity, timestamp, componentId, buf.toBinary(), buffer);
}
else {
crdt_1.DeleteComponent.write(entity, componentId, timestamp, buffer);
}
}
};
}
exports.createDumpLwwFunctionFromCrdt = createDumpLwwFunctionFromCrdt;
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 crdt_1.ProcessMessageResultType.StateUpdatedTimestamp;
}
// Outdated Message. Resend our state message through the wire.
if (currentTimestamp > timestamp) {
// console.log('2', currentTimestamp, timestamp)
return crdt_1.ProcessMessageResultType.StateOutdatedTimestamp;
}
// Deletes are idempotent
if (message.type === crdt_1.CrdtMessageType.DELETE_COMPONENT && !data.has(entityId)) {
return crdt_1.ProcessMessageResultType.NoChanges;
}
let currentDataGreater = 0;
if (data.has(entityId)) {
const writeBuffer = new ByteBuffer_1.ReadWriteByteBuffer();
schema.serialize(data.get(entityId), writeBuffer);
currentDataGreater = (0, utils_1.dataCompare)(writeBuffer.toBinary(), message.data || null);
}
else {
currentDataGreater = (0, utils_1.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 crdt_1.ProcessMessageResultType.NoChanges;
}
else if (currentDataGreater > 0) {
// Current data is greater
return crdt_1.ProcessMessageResultType.StateOutdatedData;
}
else {
// Curent data is lower
return crdt_1.ProcessMessageResultType.StateUpdatedData;
}
}
return (msg) => {
/* istanbul ignore next */
if (msg.type !== crdt_1.CrdtMessageType.PUT_COMPONENT &&
msg.type !== crdt_1.CrdtMessageType.PUT_COMPONENT_NETWORK &&
msg.type !== crdt_1.CrdtMessageType.DELETE_COMPONENT &&
msg.type !== crdt_1.CrdtMessageType.DELETE_COMPONENT_NETWORK)
/* istanbul ignore next */
return [null, data.get(msg.entityId)];
const action = crdtRuleForCurrentState(msg);
const entity = msg.entityId;
switch (action) {
case crdt_1.ProcessMessageResultType.StateUpdatedData:
case crdt_1.ProcessMessageResultType.StateUpdatedTimestamp: {
timestamps.set(entity, msg.timestamp);
if (msg.type === crdt_1.CrdtMessageType.PUT_COMPONENT || msg.type === crdt_1.CrdtMessageType.PUT_COMPONENT_NETWORK) {
const buf = new ByteBuffer_1.ReadWriteByteBuffer(msg.data);
data.set(entity, schema.deserialize(buf));
}
else {
data.delete(entity);
}
return [null, data.get(entity)];
}
case crdt_1.ProcessMessageResultType.StateOutdatedTimestamp:
case crdt_1.ProcessMessageResultType.StateOutdatedData: {
if (data.has(entity)) {
const writeBuffer = new ByteBuffer_1.ReadWriteByteBuffer();
schema.serialize(data.get(entity), writeBuffer);
return [
{
type: crdt_1.CrdtMessageType.PUT_COMPONENT,
componentId,
data: writeBuffer.toBinary(),
entityId: entity,
timestamp: timestamps.get(entity)
},
data.get(entity)
];
}
else {
return [
{
type: crdt_1.CrdtMessageType.DELETE_COMPONENT,
componentId,
entityId: entity,
timestamp: timestamps.get(entity)
},
undefined
];
}
}
}
return [null, data.get(entity)];
};
}
exports.createUpdateLwwFromCrdt = createUpdateLwwFromCrdt;
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 ByteBuffer_1.ReadWriteByteBuffer();
schema.serialize(data.get(entity), writeBuffer);
const msg = {
type: crdt_1.CrdtMessageType.PUT_COMPONENT,
componentId,
entityId: entity,
data: writeBuffer.toBinary(),
timestamp: newTimestamp
};
yield msg;
}
else {
const msg = {
type: crdt_1.CrdtMessageType.DELETE_COMPONENT,
componentId,
entityId: entity,
timestamp: newTimestamp
};
yield msg;
}
}
dirtyIterator.clear();
};
}
exports.createGetCrdtMessagesForLww = createGetCrdtMessagesForLww;
/**
* @internal
*/
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 ? (0, readonly_1.deepReadonly)(component) : null;
},
get(entity) {
const component = data.get(entity);
if (!component) {
throw new Error(`[getFrom] Component ${componentName} for entity #${entity} not found`);
}
return (0, readonly_1.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);
}
}
};
}
exports.createComponentDefinitionFromSchema = createComponentDefinitionFromSchema;