UNPKG

@dcl/ecs

Version:
159 lines (158 loc) 5.3 kB
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; }