@dcl/ecs 7.0.6-4137912823.commit-aa69b28 → 7.0.6-4153633895.commit-4aad233

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,7 +46,8 @@ export const TransformSchema = {
46
46
  return {
47
47
  position: { x: 0, y: 0, z: 0 },
48
48
  scale: { x: 1, y: 1, z: 1 },
49
- rotation: { x: 0, y: 0, z: 0, w: 1 }
49
+ rotation: { x: 0, y: 0, z: 0, w: 1 },
50
+ parent: 0
50
51
  };
51
52
  },
52
53
  extend(value) {
@@ -54,6 +55,7 @@ export const TransformSchema = {
54
55
  position: { x: 0, y: 0, z: 0 },
55
56
  scale: { x: 1, y: 1, z: 1 },
56
57
  rotation: { x: 0, y: 0, z: 0, w: 1 },
58
+ parent: 0,
57
59
  ...value
58
60
  };
59
61
  }
@@ -1,4 +1,5 @@
1
- import { ByteBuffer } from '../serialization/ByteBuffer';
1
+ import type { ISchema } from '../schemas/ISchema';
2
+ import { CrdtMessageBody, DeleteComponentMessageBody, PutComponentMessageBody } from '../serialization/crdt';
2
3
  import { Entity } from './entity';
3
4
  import { DeepReadonly } from './readonly';
4
5
  /**
@@ -48,6 +49,14 @@ export interface ComponentDefinition<T> {
48
49
  * @param entity - Entity to delete the component from
49
50
  */
50
51
  deleteFrom(entity: Entity): T | null;
52
+ /**
53
+ * @public
54
+ * Marks the entity as deleted and signals it cannot be used ever again. It must
55
+ * clear the component internal state, produces a synchronization message to remove
56
+ * the component from the entity.
57
+ * @param entity - Entity to delete the component from
58
+ */
59
+ entityDeleted(entity: Entity, markAsDirty: boolean): void;
51
60
  /**
52
61
  * Get the mutable component of the entity, throw an error if the entity doesn't have the component.
53
62
  * - Internal comment: This method adds the &lt;entity,component&gt; to the list to be reviewed next frame
@@ -60,5 +69,21 @@ export interface ComponentDefinition<T> {
60
69
  * @param entity - Entity to get the component from
61
70
  */
62
71
  getMutableOrNull(entity: Entity): T | null;
63
- writeToByteBuffer(entity: Entity, buffer: ByteBuffer): void;
72
+ /**
73
+ * This function receives a CRDT update and returns a touple with a "conflict
74
+ * resoluton" message, in case of the sender being updated or null in case of noop/accepted
75
+ * change. The second element of the touple is the modified/changed/deleted value.
76
+ * @public
77
+ */
78
+ updateFromCrdt(body: CrdtMessageBody): [null | PutComponentMessageBody | DeleteComponentMessageBody, T | null];
79
+ /**
80
+ * This function returns an iterable with all the CRDT updates that need to be
81
+ * broadcasted to other actors in the system. After returning, this function
82
+ * clears the internal dirty state. Updates are produced only once.
83
+ * @public
84
+ */
85
+ getCrdtUpdates(): Iterable<CrdtMessageBody>;
64
86
  }
87
+ export declare function incrementTimestamp(entity: Entity, timestamps: Map<Entity, number>): number;
88
+ export declare function createUpdateFromCrdt(componentId: number, timestamps: Map<Entity, number>, schema: Pick<ISchema<any>, 'serialize' | 'deserialize'>, data: Map<Entity, unknown>): (msg: CrdtMessageBody) => [null | PutComponentMessageBody | DeleteComponentMessageBody, any];
89
+ export declare function createGetCrdtMessages(componentId: number, timestamps: Map<Entity, number>, dirtyIterator: Set<Entity>, schema: Pick<ISchema<any>, 'serialize'>, data: Map<Entity, unknown>): () => Generator<PutComponentMessageBody | DeleteComponentMessageBody, void, unknown>;
@@ -1,11 +1,149 @@
1
1
  import { ReadWriteByteBuffer } from '../serialization/ByteBuffer';
2
+ import { CrdtMessageType, ProcessMessageResultType } from '../serialization/crdt';
3
+ import { dataCompare } from '../systems/crdt/utils';
2
4
  import { deepReadonly } from './readonly';
5
+ export function incrementTimestamp(entity, timestamps) {
6
+ const newTimestamp = (timestamps.get(entity) || 0) + 1;
7
+ timestamps.set(entity, newTimestamp);
8
+ return newTimestamp;
9
+ }
10
+ export function createUpdateFromCrdt(componentId, timestamps, schema, data) {
11
+ /**
12
+ * Process the received message only if the lamport number recieved is higher
13
+ * than the stored one. If its lower, we spread it to the network to correct the peer.
14
+ * If they are equal, the bigger raw data wins.
15
+
16
+ * Returns the recieved data if the lamport number was bigger than ours.
17
+ * If it was an outdated message, then we return void
18
+ * @public
19
+ */
20
+ function crdtRuleForCurrentState(message) {
21
+ const { entityId, timestamp } = message;
22
+ const currentTimestamp = timestamps.get(entityId);
23
+ // The received message is > than our current value, update our state.components.
24
+ if (currentTimestamp === undefined || currentTimestamp < timestamp) {
25
+ return ProcessMessageResultType.StateUpdatedTimestamp;
26
+ }
27
+ // Outdated Message. Resend our state message through the wire.
28
+ if (currentTimestamp > timestamp) {
29
+ // console.log('2', currentTimestamp, timestamp)
30
+ return ProcessMessageResultType.StateOutdatedTimestamp;
31
+ }
32
+ // Deletes are idempotent
33
+ if (message.type === CrdtMessageType.DELETE_COMPONENT && !data.has(entityId)) {
34
+ return ProcessMessageResultType.NoChanges;
35
+ }
36
+ let currentDataGreater = 0;
37
+ if (data.has(entityId)) {
38
+ const writeBuffer = new ReadWriteByteBuffer();
39
+ schema.serialize(data.get(entityId), writeBuffer);
40
+ currentDataGreater = dataCompare(writeBuffer.toBinary(), message.data || null);
41
+ }
42
+ else {
43
+ currentDataGreater = dataCompare(null, message.data);
44
+ }
45
+ // Same data, same timestamp. Weirdo echo message.
46
+ // console.log('3', currentDataGreater, writeBuffer.toBinary(), (message as any).data || null)
47
+ if (currentDataGreater === 0) {
48
+ return ProcessMessageResultType.NoChanges;
49
+ }
50
+ else if (currentDataGreater > 0) {
51
+ // Current data is greater
52
+ return ProcessMessageResultType.StateOutdatedData;
53
+ }
54
+ else {
55
+ // Curent data is lower
56
+ return ProcessMessageResultType.StateUpdatedData;
57
+ }
58
+ }
59
+ return (msg) => {
60
+ /* istanbul ignore next */
61
+ if (msg.type !== CrdtMessageType.PUT_COMPONENT && msg.type !== CrdtMessageType.DELETE_COMPONENT)
62
+ /* istanbul ignore next */
63
+ return [null, data.get(msg.entityId)];
64
+ const action = crdtRuleForCurrentState(msg);
65
+ const entity = msg.entityId;
66
+ switch (action) {
67
+ case ProcessMessageResultType.StateUpdatedData:
68
+ case ProcessMessageResultType.StateUpdatedTimestamp: {
69
+ timestamps.set(entity, msg.timestamp);
70
+ if (msg.type === CrdtMessageType.PUT_COMPONENT) {
71
+ const buf = new ReadWriteByteBuffer(msg.data);
72
+ data.set(entity, schema.deserialize(buf));
73
+ }
74
+ else {
75
+ data.delete(entity);
76
+ }
77
+ return [null, data.get(entity)];
78
+ }
79
+ case ProcessMessageResultType.StateOutdatedTimestamp:
80
+ case ProcessMessageResultType.StateOutdatedData: {
81
+ if (data.has(entity)) {
82
+ const writeBuffer = new ReadWriteByteBuffer();
83
+ schema.serialize(data.get(entity), writeBuffer);
84
+ return [
85
+ {
86
+ type: CrdtMessageType.PUT_COMPONENT,
87
+ componentId,
88
+ data: writeBuffer.toBinary(),
89
+ entityId: entity,
90
+ timestamp: timestamps.get(entity)
91
+ },
92
+ data.get(entity)
93
+ ];
94
+ }
95
+ else {
96
+ return [
97
+ {
98
+ type: CrdtMessageType.DELETE_COMPONENT,
99
+ componentId,
100
+ entityId: entity,
101
+ timestamp: timestamps.get(entity)
102
+ },
103
+ undefined
104
+ ];
105
+ }
106
+ }
107
+ }
108
+ return [null, data.get(entity)];
109
+ };
110
+ }
111
+ export function createGetCrdtMessages(componentId, timestamps, dirtyIterator, schema, data) {
112
+ return function* () {
113
+ for (const entity of dirtyIterator) {
114
+ const newTimestamp = incrementTimestamp(entity, timestamps);
115
+ if (data.has(entity)) {
116
+ const writeBuffer = new ReadWriteByteBuffer();
117
+ schema.serialize(data.get(entity), writeBuffer);
118
+ const msg = {
119
+ type: CrdtMessageType.PUT_COMPONENT,
120
+ componentId,
121
+ entityId: entity,
122
+ data: writeBuffer.toBinary(),
123
+ timestamp: newTimestamp
124
+ };
125
+ yield msg;
126
+ }
127
+ else {
128
+ const msg = {
129
+ type: CrdtMessageType.DELETE_COMPONENT,
130
+ componentId,
131
+ entityId: entity,
132
+ timestamp: newTimestamp
133
+ };
134
+ yield msg;
135
+ }
136
+ }
137
+ dirtyIterator.clear();
138
+ };
139
+ }
3
140
  /**
4
141
  * @internal
5
142
  */
6
143
  export function createComponentDefinitionFromSchema(componentName, componentId, schema) {
7
144
  const data = new Map();
8
145
  const dirtyIterator = new Set();
146
+ const timestamps = new Map();
9
147
  return {
10
148
  get componentId() {
11
149
  return componentId;
@@ -24,15 +162,16 @@ export function createComponentDefinitionFromSchema(componentName, componentId,
24
162
  },
25
163
  deleteFrom(entity, markAsDirty = true) {
26
164
  const component = data.get(entity);
27
- data.delete(entity);
28
- if (markAsDirty) {
165
+ if (data.delete(entity) && markAsDirty) {
29
166
  dirtyIterator.add(entity);
30
167
  }
31
- else {
32
- dirtyIterator.delete(entity);
33
- }
34
168
  return component || null;
35
169
  },
170
+ entityDeleted(entity, markAsDirty) {
171
+ if (data.delete(entity) && markAsDirty) {
172
+ dirtyIterator.add(entity);
173
+ }
174
+ },
36
175
  getOrNull(entity) {
37
176
  const component = data.get(entity);
38
177
  return component ? deepReadonly(component) : null;
@@ -85,6 +224,7 @@ export function createComponentDefinitionFromSchema(componentName, componentId,
85
224
  yield entity;
86
225
  }
87
226
  },
227
+ getCrdtUpdates: createGetCrdtMessages(componentId, timestamps, dirtyIterator, schema, data),
88
228
  toBinary(entity) {
89
229
  const component = data.get(entity);
90
230
  if (!component) {
@@ -94,45 +234,9 @@ export function createComponentDefinitionFromSchema(componentName, componentId,
94
234
  schema.serialize(component, writeBuffer);
95
235
  return writeBuffer;
96
236
  },
97
- toBinaryOrNull(entity) {
98
- const component = data.get(entity);
99
- if (!component) {
100
- return null;
101
- }
102
- const writeBuffer = new ReadWriteByteBuffer();
103
- schema.serialize(component, writeBuffer);
104
- return writeBuffer;
105
- },
106
- writeToByteBuffer(entity, buffer) {
107
- const component = data.get(entity);
108
- if (!component) {
109
- throw new Error(`[writeToByteBuffer] Component ${componentName} for entity #${entity} not found`);
110
- }
111
- schema.serialize(component, buffer);
112
- },
113
- updateFromBinary(entity, buffer, markAsDirty = true) {
114
- const component = data.get(entity);
115
- if (!component) {
116
- throw new Error(`[updateFromBinary] Component ${componentName} for ${entity} not found`);
117
- }
118
- return this.upsertFromBinary(entity, buffer, markAsDirty);
119
- },
120
- upsertFromBinary(entity, buffer, markAsDirty = true) {
121
- const newValue = schema.deserialize(buffer);
122
- data.set(entity, newValue);
123
- if (markAsDirty) {
124
- dirtyIterator.add(entity);
125
- }
126
- else {
127
- dirtyIterator.delete(entity);
128
- }
129
- return newValue;
130
- },
237
+ updateFromCrdt: createUpdateFromCrdt(componentId, timestamps, schema, data),
131
238
  deserialize(buffer) {
132
239
  return schema.deserialize(buffer);
133
- },
134
- clearDirty() {
135
- dirtyIterator.clear();
136
240
  }
137
241
  };
138
242
  }
@@ -1,4 +1,4 @@
1
- import { createGSet } from '@dcl/crdt/dist/gset';
1
+ import { createVersionGSet } from '../systems/crdt/gset';
2
2
  /**
3
3
  * @internal
4
4
  */
@@ -64,7 +64,7 @@ export function EntityContainer() {
64
64
  let entityCounter = RESERVED_STATIC_ENTITIES;
65
65
  const usedEntities = new Set();
66
66
  let toRemoveEntities = [];
67
- const removedEntities = createGSet();
67
+ const removedEntities = createVersionGSet();
68
68
  function generateNewEntity() {
69
69
  if (entityCounter > MAX_ENTITY_NUMBER - 1) {
70
70
  throw new Error(`It fails trying to generate an entity out of range ${MAX_ENTITY_NUMBER}.`);
@@ -109,10 +109,12 @@ export function EntityContainer() {
109
109
  }
110
110
  function releaseRemovedEntities() {
111
111
  const arr = toRemoveEntities;
112
- toRemoveEntities = [];
113
- for (const entity of arr) {
114
- const [n, v] = EntityUtils.fromEntityId(entity);
115
- removedEntities.addTo(n, v);
112
+ if (arr.length) {
113
+ toRemoveEntities = [];
114
+ for (const entity of arr) {
115
+ const [n, v] = EntityUtils.fromEntityId(entity);
116
+ removedEntities.addTo(n, v);
117
+ }
116
118
  }
117
119
  return arr;
118
120
  }
@@ -128,10 +130,9 @@ export function EntityContainer() {
128
130
  }
129
131
  function updateUsedEntity(entity) {
130
132
  const [n, v] = EntityUtils.fromEntityId(entity);
131
- const removedVersion = removedEntities.getMap().get(n);
132
- if (removedVersion !== undefined && removedVersion >= v) {
133
+ // if the entity was removed then abort fast
134
+ if (removedEntities.has(n, v))
133
135
  return false;
134
- }
135
136
  // Update
136
137
  if (v > 0) {
137
138
  for (let i = 0; i <= v - 1; i++) {
@@ -26,9 +26,7 @@ function preEngine() {
26
26
  }
27
27
  function removeEntity(entity) {
28
28
  for (const [, component] of componentsDefinition) {
29
- if (component.has(entity)) {
30
- component.deleteFrom(entity);
31
- }
29
+ component.entityDeleted(entity, true);
32
30
  }
33
31
  return entityContainer.removeEntity(entity);
34
32
  }
@@ -184,12 +182,9 @@ export function Engine(options) {
184
182
  const ret = system.fn(dt);
185
183
  checkNotThenable(ret, `A system (${system.name || 'anonymous'}) returned a thenable. Systems cannot be async functions. Documentation: https://dcl.gg/sdk/sync-systems`);
186
184
  }
187
- const dirtyEntities = crdtSystem.updateState();
185
+ // get the deleted entities to send the DeleteEntity CRDT commands
188
186
  const deletedEntites = partialEngine.entityContainer.releaseRemovedEntities();
189
- await crdtSystem.sendMessages(dirtyEntities, deletedEntites);
190
- for (const definition of partialEngine.componentsIter()) {
191
- definition.clearDirty();
192
- }
187
+ await crdtSystem.sendMessages(deletedEntites);
193
188
  }
194
189
  return {
195
190
  addEntity: partialEngine.addEntity,
@@ -212,7 +207,6 @@ export function Engine(options) {
212
207
  CameraEntity: 2,
213
208
  getEntityState: partialEngine.entityContainer.getEntityState,
214
209
  addTransport: crdtSystem.addTransport,
215
- getCrdtState: crdtSystem.getCrdt,
216
210
  entityContainer: partialEngine.entityContainer
217
211
  };
218
212
  }
@@ -10,18 +10,18 @@ export var PutComponentOperation;
10
10
  * Call this function for an optimal writing data passing the ByteBuffer
11
11
  * already allocated
12
12
  */
13
- function write(entity, timestamp, componentDefinition, buf) {
13
+ function write(entity, timestamp, componentId, data, buf) {
14
14
  // reserve the beginning
15
15
  const startMessageOffset = buf.incrementWriteOffset(CRDT_MESSAGE_HEADER_LENGTH + PutComponentOperation.MESSAGE_HEADER_LENGTH);
16
16
  // write body
17
- componentDefinition.writeToByteBuffer(entity, buf);
17
+ buf.writeBuffer(data, false);
18
18
  const messageLength = buf.currentWriteOffset() - startMessageOffset;
19
19
  // Write CrdtMessage header
20
20
  buf.setUint32(startMessageOffset, messageLength);
21
21
  buf.setUint32(startMessageOffset + 4, CrdtMessageType.PUT_COMPONENT);
22
22
  // Write ComponentOperation header
23
23
  buf.setUint32(startMessageOffset + 8, entity);
24
- buf.setUint32(startMessageOffset + 12, componentDefinition.componentId);
24
+ buf.setUint32(startMessageOffset + 12, componentId);
25
25
  buf.setUint32(startMessageOffset + 16, timestamp);
26
26
  const newLocal = messageLength - PutComponentOperation.MESSAGE_HEADER_LENGTH - CRDT_MESSAGE_HEADER_LENGTH;
27
27
  buf.setUint32(startMessageOffset + 20, newLocal);
@@ -1,4 +1,4 @@
1
- import { Entity } from '../../engine/entity';
1
+ import { Entity, uint32 } from '../../engine/entity';
2
2
  /**
3
3
  * @public
4
4
  */
@@ -9,6 +9,21 @@ export declare enum CrdtMessageType {
9
9
  DELETE_ENTITY = 3,
10
10
  MAX_MESSAGE_TYPE = 4
11
11
  }
12
+ /**
13
+ * Min length = 8 bytes
14
+ * All message length including
15
+ * @param length - uint32 the length of all message (including the header)
16
+ * @param type - define the function which handles the data
17
+ * @public
18
+ */
19
+ export type CrdtMessageHeader = {
20
+ length: uint32;
21
+ type: uint32;
22
+ };
23
+ /**
24
+ * @public
25
+ */
26
+ export declare const CRDT_MESSAGE_HEADER_LENGTH = 8;
12
27
  /**
13
28
  * Min. length = header (8 bytes) + 16 bytes = 24 bytes
14
29
  *
@@ -45,7 +60,67 @@ export type DeleteEntityMessageBody = {
45
60
  type: CrdtMessageType.DELETE_ENTITY;
46
61
  entityId: Entity;
47
62
  };
63
+ /**
64
+ * @public
65
+ */
66
+ export type PutComponentMessage = CrdtMessageHeader & PutComponentMessageBody;
67
+ /**
68
+ * @public
69
+ */
70
+ export type DeleteComponentMessage = CrdtMessageHeader & DeleteComponentMessageBody;
71
+ /**
72
+ * @public
73
+ */
74
+ export type DeleteEntityMessage = CrdtMessageHeader & DeleteEntityMessageBody;
75
+ /**
76
+ * @public
77
+ */
78
+ export type CrdtMessage = PutComponentMessage | DeleteComponentMessage | DeleteEntityMessage;
48
79
  /**
49
80
  * @public
50
81
  */
51
82
  export type CrdtMessageBody = PutComponentMessageBody | DeleteComponentMessageBody | DeleteEntityMessageBody;
83
+ export declare enum ProcessMessageResultType {
84
+ /**
85
+ * Typical message and new state set.
86
+ * @state CHANGE
87
+ * @reason Incoming message has a timestamp greater
88
+ */
89
+ StateUpdatedTimestamp = 1,
90
+ /**
91
+ * Typical message when it is considered old.
92
+ * @state it does NOT CHANGE.
93
+ * @reason incoming message has a timestamp lower.
94
+ */
95
+ StateOutdatedTimestamp = 2,
96
+ /**
97
+ * Weird message, same timestamp and data.
98
+ * @state it does NOT CHANGE.
99
+ * @reason consistent state between peers.
100
+ */
101
+ NoChanges = 3,
102
+ /**
103
+ * Less but typical message, same timestamp, resolution by data.
104
+ * @state it does NOT CHANGE.
105
+ * @reason incoming message has a LOWER data.
106
+ */
107
+ StateOutdatedData = 4,
108
+ /**
109
+ * Less but typical message, same timestamp, resolution by data.
110
+ * @state CHANGE.
111
+ * @reason incoming message has a GREATER data.
112
+ */
113
+ StateUpdatedData = 5,
114
+ /**
115
+ * Entity was previously deleted.
116
+ * @state it does NOT CHANGE.
117
+ * @reason The message is considered old.
118
+ */
119
+ EntityWasDeleted = 6,
120
+ /**
121
+ * Entity should be deleted.
122
+ * @state CHANGE.
123
+ * @reason the state is storing old entities
124
+ */
125
+ EntityDeleted = 7
126
+ }
@@ -11,6 +11,63 @@ export var CrdtMessageType;
11
11
  CrdtMessageType[CrdtMessageType["MAX_MESSAGE_TYPE"] = 4] = "MAX_MESSAGE_TYPE";
12
12
  })(CrdtMessageType || (CrdtMessageType = {}));
13
13
  /**
14
- * @internal
14
+ * @public
15
15
  */
16
16
  export const CRDT_MESSAGE_HEADER_LENGTH = 8;
17
+ export var ProcessMessageResultType;
18
+ (function (ProcessMessageResultType) {
19
+ /**
20
+ * Typical message and new state set.
21
+ * @state CHANGE
22
+ * @reason Incoming message has a timestamp greater
23
+ */
24
+ ProcessMessageResultType[ProcessMessageResultType["StateUpdatedTimestamp"] = 1] = "StateUpdatedTimestamp";
25
+ /**
26
+ * Typical message when it is considered old.
27
+ * @state it does NOT CHANGE.
28
+ * @reason incoming message has a timestamp lower.
29
+ */
30
+ ProcessMessageResultType[ProcessMessageResultType["StateOutdatedTimestamp"] = 2] = "StateOutdatedTimestamp";
31
+ /**
32
+ * Weird message, same timestamp and data.
33
+ * @state it does NOT CHANGE.
34
+ * @reason consistent state between peers.
35
+ */
36
+ ProcessMessageResultType[ProcessMessageResultType["NoChanges"] = 3] = "NoChanges";
37
+ /**
38
+ * Less but typical message, same timestamp, resolution by data.
39
+ * @state it does NOT CHANGE.
40
+ * @reason incoming message has a LOWER data.
41
+ */
42
+ ProcessMessageResultType[ProcessMessageResultType["StateOutdatedData"] = 4] = "StateOutdatedData";
43
+ /**
44
+ * Less but typical message, same timestamp, resolution by data.
45
+ * @state CHANGE.
46
+ * @reason incoming message has a GREATER data.
47
+ */
48
+ ProcessMessageResultType[ProcessMessageResultType["StateUpdatedData"] = 5] = "StateUpdatedData";
49
+ /**
50
+ * Entity was previously deleted.
51
+ * @state it does NOT CHANGE.
52
+ * @reason The message is considered old.
53
+ */
54
+ ProcessMessageResultType[ProcessMessageResultType["EntityWasDeleted"] = 6] = "EntityWasDeleted";
55
+ /**
56
+ * Entity should be deleted.
57
+ * @state CHANGE.
58
+ * @reason the state is storing old entities
59
+ */
60
+ ProcessMessageResultType[ProcessMessageResultType["EntityDeleted"] = 7] = "EntityDeleted";
61
+ })(ProcessMessageResultType || (ProcessMessageResultType = {}));
62
+ // we receive LWW, v=6, we have v=5 => we receive with delay the deleteEntity(v=5)
63
+ // => we should generate the deleteEntity message effects internally with deleteEntity(v=5),
64
+ // but don't resend the deleteEntity
65
+ // - (CRDT) addDeletedEntitySet v=5 (with crdt state cleaning) and then LWW v=6
66
+ // - (engine) engine.deleteEntity v=5
67
+ // we receive LWW, v=7, we have v=5 => we receive with delay the deleteEntity(v=5), deleteEntity(v=6), ..., N
68
+ // => we should generate the deleteEntity message effects internally with deleteEntity(v=5),
69
+ // but don't resend the deleteEntity
70
+ // - (CRDT) addDeletedEntitySet v=5 (with crdt state cleaning) and then LWW v=6
71
+ // - (engine) engine.deleteEntity v=5
72
+ // msg delete entity: it only should be sent by deleter
73
+ //
@@ -0,0 +1,29 @@
1
+ /**
2
+ * This Grow-only set is a implementation with a memory optimization.
3
+ *
4
+ * Each number has a version, no matter how the final compound number is mixed.
5
+ *
6
+ * The function `add` isn't defined (for this implementation), instead, the addTo is: add all versions of a number `n` until (and including) `v`.
7
+ *
8
+ */
9
+ export type OptimizedGrowonlySet = {
10
+ /**
11
+ * @public
12
+ *
13
+ * @param n
14
+ * @param v
15
+ * @returns
16
+ */
17
+ addTo(n: number, v: number): boolean;
18
+ /**
19
+ * @public
20
+ *
21
+ * @returns the set with [number, version] of each value
22
+ */
23
+ has(n: number, v: number): boolean;
24
+ };
25
+ /**
26
+ *
27
+ * @returns a new GSet
28
+ */
29
+ export declare function createVersionGSet(): OptimizedGrowonlySet;
@@ -0,0 +1,50 @@
1
+ /**
2
+ *
3
+ * @returns a new GSet
4
+ */
5
+ export function createVersionGSet() {
6
+ const lastVersion = new Map();
7
+ return {
8
+ /**
9
+ *
10
+ * @param number
11
+ * @param version
12
+ * @returns
13
+ */
14
+ addTo(number, version) {
15
+ /* istanbul ignore next */
16
+ if (version < 0) {
17
+ /* istanbul ignore next */
18
+ return false;
19
+ }
20
+ const currentValue = lastVersion.get(number);
21
+ // If the version is >=, it means the value it's already in the set
22
+ if (currentValue !== undefined && currentValue >= version) {
23
+ return true;
24
+ }
25
+ lastVersion.set(number, version);
26
+ return true;
27
+ },
28
+ /**
29
+ * @returns the set with [number, version] of each value
30
+ */
31
+ has(n, v) {
32
+ const currentValue = lastVersion.get(n);
33
+ // If the version is >=, it means the value it's already in the set
34
+ if (currentValue !== undefined && currentValue >= v) {
35
+ return true;
36
+ }
37
+ return false;
38
+ },
39
+ /**
40
+ * Warning: this function returns the reference to the internal map,
41
+ * if you need to mutate some value, make a copy.
42
+ * For optimization purpose the copy isn't made here.
43
+ *
44
+ * @returns the map of number to version
45
+ */
46
+ getMap() {
47
+ return lastVersion;
48
+ }
49
+ };
50
+ }
@@ -1,6 +1,4 @@
1
- import { crdtProtocol } from '@dcl/crdt';
2
- import { CRDTMessageType, ProcessMessageResultType } from '@dcl/crdt/dist/types';
3
- import { EntityState, EntityUtils } from '../../engine/entity';
1
+ import { EntityState } from '../../engine/entity';
4
2
  import { ReadWriteByteBuffer } from '../../serialization/ByteBuffer';
5
3
  import { CrdtMessageProtocol } from '../../serialization/crdt';
6
4
  import { DeleteComponent } from '../../serialization/crdt/deleteComponent';
@@ -12,11 +10,6 @@ import { CrdtMessageType } from '../../serialization/crdt/types';
12
10
  */
13
11
  export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
14
12
  const transports = [];
15
- // CRDT Client
16
- const crdtClient = crdtProtocol({
17
- toEntityId: EntityUtils.toEntityId,
18
- fromEntityId: EntityUtils.fromEntityId
19
- });
20
13
  // Messages that we received at transport.onMessage waiting to be processed
21
14
  const receivedMessages = [];
22
15
  // Messages already processed by the engine but that we need to broadcast to other transports.
@@ -93,20 +86,9 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
93
86
  const entitiesShouldBeCleaned = [];
94
87
  for (const msg of messagesToProcess) {
95
88
  if (msg.type === CrdtMessageType.DELETE_ENTITY) {
96
- crdtClient.processMessage({
97
- type: CRDTMessageType.CRDTMT_DeleteEntity,
98
- entityId: msg.entityId
99
- });
100
89
  entitiesShouldBeCleaned.push(msg.entityId);
101
90
  }
102
91
  else {
103
- const crdtMessage = {
104
- type: CRDTMessageType.CRDTMT_PutComponentData,
105
- entityId: msg.entityId,
106
- componentId: msg.componentId,
107
- data: msg.type === CrdtMessageType.PUT_COMPONENT ? msg.data : null,
108
- timestamp: msg.timestamp
109
- };
110
92
  const entityState = engine.entityContainer.getEntityState(msg.entityId);
111
93
  // Skip updates from removed entityes
112
94
  if (entityState === EntityState.Removed)
@@ -116,146 +98,73 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
116
98
  engine.entityContainer.updateUsedEntity(msg.entityId);
117
99
  }
118
100
  const component = engine.getComponentOrNull(msg.componentId);
119
- // The state isn't updated because the dirty was set
120
- // out of the block of systems update between `receiveMessage` and `updateState`
121
- if (component?.isDirty(msg.entityId)) {
122
- crdtClient.createComponentDataEvent(component.componentId, msg.entityId, component.toBinaryOrNull(msg.entityId)?.toBinary() || null);
123
- }
124
- const processResult = crdtClient.processMessage(crdtMessage);
125
- if (!component) {
126
- continue;
127
- }
128
- switch (processResult) {
129
- case ProcessMessageResultType.StateUpdatedTimestamp:
130
- case ProcessMessageResultType.StateUpdatedData:
131
- // Add message to transport queue to be processed by others transports
132
- broadcastMessages.push(msg);
133
- // Process CRDT Message
134
- if (msg.type === CrdtMessageType.DELETE_COMPONENT) {
135
- component.deleteFrom(msg.entityId, false);
101
+ if (component) {
102
+ const [conflictMessage] = component.updateFromCrdt(msg);
103
+ if (conflictMessage) {
104
+ const offset = bufferForOutdated.currentWriteOffset();
105
+ if (conflictMessage.type === CrdtMessageType.PUT_COMPONENT) {
106
+ PutComponentOperation.write(msg.entityId, conflictMessage.timestamp, conflictMessage.componentId, conflictMessage.data, bufferForOutdated);
136
107
  }
137
108
  else {
138
- const data = new ReadWriteByteBuffer(msg.data);
139
- component.upsertFromBinary(msg.entityId, data, false);
109
+ DeleteComponent.write(msg.entityId, component.componentId, conflictMessage.timestamp, bufferForOutdated);
140
110
  }
111
+ outdatedMessages.push({
112
+ ...msg,
113
+ messageBuffer: bufferForOutdated.buffer().subarray(offset, bufferForOutdated.currentWriteOffset())
114
+ });
115
+ }
116
+ else {
117
+ // Add message to transport queue to be processed by others transports
118
+ broadcastMessages.push(msg);
141
119
  onProcessEntityComponentChange && onProcessEntityComponentChange(msg.entityId, msg.type, component);
142
- break;
143
- // CRDT outdated message. Resend this message to the transport
144
- // To do this we add this message to a queue that will be processed at the end of the update tick
145
- case ProcessMessageResultType.StateOutdatedData:
146
- case ProcessMessageResultType.StateOutdatedTimestamp:
147
- const current = crdtClient.getState().components.get(msg.componentId)?.get(msg.entityId);
148
- if (current) {
149
- const offset = bufferForOutdated.currentWriteOffset();
150
- const ts = current.timestamp;
151
- if (component.has(msg.entityId)) {
152
- PutComponentOperation.write(msg.entityId, ts, component, bufferForOutdated);
153
- }
154
- else {
155
- DeleteComponent.write(msg.entityId, component.componentId, ts, bufferForOutdated);
156
- }
157
- outdatedMessages.push({
158
- ...msg,
159
- messageBuffer: bufferForOutdated.buffer().subarray(offset, bufferForOutdated.currentWriteOffset())
160
- });
161
- }
162
- break;
163
- case ProcessMessageResultType.NoChanges:
164
- case ProcessMessageResultType.EntityDeleted:
165
- case ProcessMessageResultType.EntityWasDeleted:
166
- default:
167
- break;
120
+ }
168
121
  }
169
122
  }
170
123
  }
124
+ // the last stage of the syncrhonization is to delete the entities
171
125
  for (const entity of entitiesShouldBeCleaned) {
172
126
  // If we tried to resend outdated message and the entity was deleted before, we avoid sending them.
173
127
  for (let i = outdatedMessages.length - 1; i >= 0; i--) {
174
- if (outdatedMessages[i].entityId === entity) {
128
+ if (outdatedMessages[i].entityId === entity && outdatedMessages[i].type !== CrdtMessageType.DELETE_ENTITY) {
175
129
  outdatedMessages.splice(i, 1);
176
130
  }
177
131
  }
178
132
  for (const definition of engine.componentsIter()) {
179
- definition.deleteFrom(entity, false);
133
+ definition.entityDeleted(entity, false);
180
134
  }
181
135
  engine.entityContainer.updateRemovedEntity(entity);
182
136
  onProcessEntityComponentChange && onProcessEntityComponentChange(entity, CrdtMessageType.DELETE_ENTITY);
183
137
  }
184
138
  }
185
- /**
186
- * Updates CRDT state of the current engine dirty components
187
- *
188
- * TODO: optimize this function allocations using a bitmap
189
- * TODO: unify this function with sendMessages
190
- */
191
- function updateState() {
192
- const dirtyMap = new Map();
193
- for (const component of engine.componentsIter()) {
194
- let entitySet = null;
195
- for (const entity of component.dirtyIterator()) {
196
- if (!entitySet) {
197
- entitySet = [];
198
- dirtyMap.set(component, entitySet);
199
- }
200
- // TODO: reuse shared writer to prevent extra allocations of toBinary
201
- const componentValue = component.toBinaryOrNull(entity)?.toBinary() ?? null;
202
- // TODO: do not emit event if componentValue equals the value didn't change
203
- // if update goes bad, the entity doesn't accept put anymore (it's added to deleted entities set)
204
- if (crdtClient.createComponentDataEvent(component.componentId, entity, componentValue) === null) {
205
- component.deleteFrom(entity, false);
206
- }
207
- else {
208
- entitySet.push(entity);
209
- onProcessEntityComponentChange &&
210
- onProcessEntityComponentChange(entity, componentValue === null ? CrdtMessageType.DELETE_COMPONENT : CrdtMessageType.PUT_COMPONENT, component);
211
- }
212
- }
213
- }
214
- return dirtyMap;
215
- }
216
139
  /**
217
140
  * Iterates the dirty map and generates crdt messages to be send
218
141
  */
219
- async function sendMessages(dirtyEntities, deletedEntities) {
142
+ async function sendMessages(entitiesDeletedThisTick) {
220
143
  // CRDT Messages will be the merge between the recieved transport messages and the new crdt messages
221
144
  const crdtMessages = getMessages(broadcastMessages);
222
145
  const outdatedMessagesBkp = getMessages(outdatedMessages);
223
146
  const buffer = new ReadWriteByteBuffer();
224
- for (const [component, entities] of dirtyEntities) {
225
- for (const entity of entities) {
226
- // Component will be always defined here since dirtyMap its an iterator of engine.componentsDefinition
227
- const { timestamp } = crdtClient
228
- .getState()
229
- .components.get(component.componentId)
230
- .get(entity);
147
+ for (const component of engine.componentsIter()) {
148
+ for (const message of component.getCrdtUpdates()) {
231
149
  const offset = buffer.currentWriteOffset();
232
- const type = component.has(entity)
233
- ? CrdtMessageType.PUT_COMPONENT
234
- : CrdtMessageType.DELETE_COMPONENT;
235
- const transportMessage = {
236
- type,
237
- entityId: entity,
238
- componentId: component.componentId,
239
- timestamp
240
- };
241
150
  // Avoid creating messages if there is no transport that will handle it
242
- if (transports.some((t) => t.filter(transportMessage))) {
243
- if (transportMessage.type === CrdtMessageType.PUT_COMPONENT) {
244
- PutComponentOperation.write(entity, timestamp, component, buffer);
151
+ if (transports.some((t) => t.filter(message))) {
152
+ if (message.type === CrdtMessageType.PUT_COMPONENT) {
153
+ PutComponentOperation.write(message.entityId, message.timestamp, message.componentId, message.data, buffer);
245
154
  }
246
- else {
247
- DeleteComponent.write(entity, component.componentId, timestamp, buffer);
155
+ else if (message.type === CrdtMessageType.DELETE_COMPONENT) {
156
+ DeleteComponent.write(message.entityId, component.componentId, message.timestamp, buffer);
248
157
  }
249
158
  crdtMessages.push({
250
- ...transportMessage,
159
+ ...message,
251
160
  messageBuffer: buffer.buffer().subarray(offset, buffer.currentWriteOffset())
252
161
  });
162
+ onProcessEntityComponentChange && onProcessEntityComponentChange(message.entityId, message.type, component);
253
163
  }
254
164
  }
255
165
  }
256
166
  // After all updates, I execute the DeletedEntity messages
257
- for (const entityId of deletedEntities) {
258
- crdtClient.createDeleteEntityEvent(entityId);
167
+ for (const entityId of entitiesDeletedThisTick) {
259
168
  const offset = buffer.currentWriteOffset();
260
169
  DeleteEntity.write(entityId, buffer);
261
170
  crdtMessages.push({
@@ -263,6 +172,7 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
263
172
  entityId,
264
173
  messageBuffer: buffer.buffer().subarray(offset, buffer.currentWriteOffset())
265
174
  });
175
+ onProcessEntityComponentChange && onProcessEntityComponentChange(entityId, CrdtMessageType.DELETE_ENTITY);
266
176
  }
267
177
  // Send CRDT messages to transports
268
178
  const transportBuffer = new ReadWriteByteBuffer();
@@ -301,18 +211,9 @@ export function crdtSceneSystem(engine, onProcessEntityComponentChange) {
301
211
  const id = transports.push(transport) - 1;
302
212
  transport.onmessage = parseChunkMessage(id);
303
213
  }
304
- /**
305
- * @public
306
- * @returns returns the crdt state
307
- */
308
- function getCrdt() {
309
- return crdtClient.getState();
310
- }
311
214
  return {
312
- getCrdt,
313
215
  sendMessages,
314
216
  receiveMessages,
315
- addTransport,
316
- updateState
217
+ addTransport
317
218
  };
318
219
  }
@@ -9,3 +9,33 @@ export var CrdtUtils;
9
9
  })(SynchronizedEntityType = CrdtUtils.SynchronizedEntityType || (CrdtUtils.SynchronizedEntityType = {}));
10
10
  })(CrdtUtils || (CrdtUtils = {}));
11
11
  export default CrdtUtils;
12
+ /**
13
+ * Compare raw data.
14
+ * @internal
15
+ * @returns 0 if is the same data, 1 if a > b, -1 if b > a
16
+ */
17
+ export function dataCompare(a, b) {
18
+ // At reference level
19
+ if (a === b)
20
+ return 0;
21
+ if (a === null && b !== null)
22
+ return -1;
23
+ if (a !== null && b === null)
24
+ return 1;
25
+ if (a instanceof Uint8Array && b instanceof Uint8Array) {
26
+ let res;
27
+ const n = a.byteLength > b.byteLength ? b.byteLength : a.byteLength;
28
+ for (let i = 0; i < n; i++) {
29
+ res = a[i] - b[i];
30
+ if (res !== 0) {
31
+ return res > 0 ? 1 : -1;
32
+ }
33
+ }
34
+ res = a.byteLength - b.byteLength;
35
+ return res > 0 ? 1 : res < 0 ? -1 : 0;
36
+ }
37
+ if (typeof a === 'string') {
38
+ return a.localeCompare(b);
39
+ }
40
+ return a > b ? 1 : -1;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcl/ecs",
3
- "version": "7.0.6-4137912823.commit-aa69b28",
3
+ "version": "7.0.6-4153633895.commit-4aad233",
4
4
  "description": "Decentraland ECS",
5
5
  "main": "./dist/index.js",
6
6
  "typings": "./dist/index.d.ts",
@@ -27,7 +27,6 @@
27
27
  "ts-proto": "^1.112.0"
28
28
  },
29
29
  "dependencies": {
30
- "@dcl/crdt": "7.0.6-4137912823.commit-aa69b28",
31
30
  "@dcl/js-runtime": "file:../js-runtime",
32
31
  "@dcl/protocol": "1.0.0-4114477251.commit-ccb88d6"
33
32
  },
@@ -41,5 +40,5 @@
41
40
  "displayName": "ECS",
42
41
  "tsconfig": "./tsconfig.json"
43
42
  },
44
- "commit": "aa69b2868fae613a7b33b2f40e3f8eb689992c22"
43
+ "commit": "4aad23359bad8cca4eebdf0c1a236a725d82b32d"
45
44
  }