@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.
- package/dist/components/legacy/Transform.js +3 -1
- package/dist/engine/component.d.ts +27 -2
- package/dist/engine/component.js +146 -42
- package/dist/engine/entity.js +10 -9
- package/dist/engine/index.js +3 -9
- package/dist/serialization/crdt/putComponent.js +3 -3
- package/dist/serialization/crdt/types.d.ts +76 -1
- package/dist/serialization/crdt/types.js +58 -1
- package/dist/systems/crdt/gset.d.ts +29 -0
- package/dist/systems/crdt/gset.js +50 -0
- package/dist/systems/crdt/index.js +33 -132
- package/dist/systems/crdt/utils.js +30 -0
- package/package.json +2 -3
|
@@ -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 {
|
|
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 <entity,component> 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
|
-
|
|
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>;
|
package/dist/engine/component.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/engine/entity.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
-
|
|
132
|
-
if (
|
|
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++) {
|
package/dist/engine/index.js
CHANGED
|
@@ -26,9 +26,7 @@ function preEngine() {
|
|
|
26
26
|
}
|
|
27
27
|
function removeEntity(entity) {
|
|
28
28
|
for (const [, component] of componentsDefinition) {
|
|
29
|
-
|
|
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
|
-
|
|
185
|
+
// get the deleted entities to send the DeleteEntity CRDT commands
|
|
188
186
|
const deletedEntites = partialEngine.entityContainer.releaseRemovedEntities();
|
|
189
|
-
await crdtSystem.sendMessages(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
* @
|
|
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 {
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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
|
|
225
|
-
for (const
|
|
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(
|
|
243
|
-
if (
|
|
244
|
-
PutComponentOperation.write(
|
|
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(
|
|
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
|
-
...
|
|
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
|
|
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-
|
|
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": "
|
|
43
|
+
"commit": "4aad23359bad8cca4eebdf0c1a236a725d82b32d"
|
|
45
44
|
}
|