UNPKG

@dcl/ecs

Version:
289 lines (287 loc) • 11.9 kB
"use strict"; 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;