@vuer-ai/vuer-rtc 0.5.4 → 0.6.0
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/client/coalescence/index.d.ts +3 -0
- package/dist/client/coalescence/index.d.ts.map +1 -1
- package/dist/client/coalescence/index.js +3 -0
- package/dist/client/coalescence/index.js.map +1 -1
- package/dist/client/coalescence/lwwOperations.d.ts +37 -0
- package/dist/client/coalescence/lwwOperations.d.ts.map +1 -0
- package/dist/client/coalescence/lwwOperations.js +69 -0
- package/dist/client/coalescence/lwwOperations.js.map +1 -0
- package/dist/client/coalescence/numberOperations.d.ts +32 -0
- package/dist/client/coalescence/numberOperations.d.ts.map +1 -0
- package/dist/client/coalescence/numberOperations.js +66 -0
- package/dist/client/coalescence/numberOperations.js.map +1 -0
- package/dist/client/coalescence/registry.d.ts.map +1 -1
- package/dist/client/coalescence/registry.js +20 -0
- package/dist/client/coalescence/registry.js.map +1 -1
- package/dist/client/coalescence/vector3Operations.d.ts +32 -0
- package/dist/client/coalescence/vector3Operations.d.ts.map +1 -0
- package/dist/client/coalescence/vector3Operations.js +71 -0
- package/dist/client/coalescence/vector3Operations.js.map +1 -0
- package/package.json +1 -1
- package/src/client/coalescence/index.ts +3 -0
- package/src/client/coalescence/lwwOperations.ts +104 -0
- package/src/client/coalescence/numberOperations.ts +80 -0
- package/src/client/coalescence/registry.ts +22 -0
- package/src/client/coalescence/vector3Operations.ts +85 -0
- package/tests/client/graph-coalescence-phase1.test.ts +357 -0
|
@@ -6,4 +6,7 @@
|
|
|
6
6
|
export { coalesceOperations, registerCoalescer, getCoalescer, hasCoalescer, type CoalesceOptions, type CoalesceHandler, type TypeGuard, } from './registry.js';
|
|
7
7
|
export { coalesceTextInserts, isTextInsertOp, type TextInsertOp } from './textInserts.js';
|
|
8
8
|
export { coalesceTextDeletes, isTextDeleteOp, type TextDeleteOp } from './textDeletes.js';
|
|
9
|
+
export { coalesceNumberAdds, isNumberAddOp, type NumberAddOp } from './numberOperations.js';
|
|
10
|
+
export { coalesceVector3Adds, isVector3AddOp, type Vector3AddOp } from './vector3Operations.js';
|
|
11
|
+
export { coalesceLWWSets, isLWWSetOp, type LWWSetOp } from './lwwOperations.js';
|
|
9
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -6,4 +6,7 @@
|
|
|
6
6
|
export { coalesceOperations, registerCoalescer, getCoalescer, hasCoalescer, } from './registry.js';
|
|
7
7
|
export { coalesceTextInserts, isTextInsertOp } from './textInserts.js';
|
|
8
8
|
export { coalesceTextDeletes, isTextDeleteOp } from './textDeletes.js';
|
|
9
|
+
export { coalesceNumberAdds, isNumberAddOp } from './numberOperations.js';
|
|
10
|
+
export { coalesceVector3Adds, isVector3AddOp } from './vector3Operations.js';
|
|
11
|
+
export { coalesceLWWSets, isLWWSetOp } from './lwwOperations.js';
|
|
9
12
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/coalescence/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,YAAY,GAIb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/client/coalescence/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,YAAY,GAIb,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAE,eAAe,EAAE,UAAU,EAAiB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-Write-Wins (LWW) Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Generic coalescer for all *.set operations.
|
|
5
|
+
* Set operations follow Last-Write-Wins semantics - only the last value matters.
|
|
6
|
+
*
|
|
7
|
+
* Supported operations:
|
|
8
|
+
* - number.set, string.set, boolean.set
|
|
9
|
+
* - vector3.set, euler.set, quaternion.set, color.set
|
|
10
|
+
* - array.set, object.set
|
|
11
|
+
*/
|
|
12
|
+
import type { Operation, NumberSetOp, StringSetOp, BooleanSetOp, Vector3SetOp, EulerSetOp, QuaternionSetOp, ColorSetOp, ArraySetOp, ObjectSetOp } from '../../operations/OperationTypes.js';
|
|
13
|
+
import type { CoalesceOptions } from './registry.js';
|
|
14
|
+
export type LWWSetOp = NumberSetOp | StringSetOp | BooleanSetOp | Vector3SetOp | EulerSetOp | QuaternionSetOp | ColorSetOp | ArraySetOp | ObjectSetOp;
|
|
15
|
+
/**
|
|
16
|
+
* Check if an operation is a *.set operation
|
|
17
|
+
*/
|
|
18
|
+
export declare function isLWWSetOp(op: Operation): op is LWWSetOp;
|
|
19
|
+
/**
|
|
20
|
+
* Coalesce consecutive *.set operations using Last-Write-Wins semantics.
|
|
21
|
+
*
|
|
22
|
+
* Since set operations follow LWW, only the last value in a sequence matters.
|
|
23
|
+
* We can safely discard all but the last operation for the same target.
|
|
24
|
+
*
|
|
25
|
+
* Example:
|
|
26
|
+
* Input: [set(key:"cube", path:"color", value:"red"), set(key:"cube", path:"color", value:"blue")]
|
|
27
|
+
* Output: [set(key:"cube", path:"color", value:"blue")]
|
|
28
|
+
*
|
|
29
|
+
* @param ops - Array of *.set operations
|
|
30
|
+
* @param options - Coalescence options
|
|
31
|
+
* NOTE: Time threshold is ignored for Last-Write-Wins operations.
|
|
32
|
+
* LWW semantics mean only the final value matters, regardless of timing,
|
|
33
|
+
* so we keep only the last operation on each target.
|
|
34
|
+
* @returns New array with coalesced operations
|
|
35
|
+
*/
|
|
36
|
+
export declare function coalesceLWWSets(ops: LWWSetOp[], options?: CoalesceOptions): LWWSetOp[];
|
|
37
|
+
//# sourceMappingURL=lwwOperations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lwwOperations.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/lwwOperations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,eAAe,EACf,UAAU,EACV,UAAU,EACV,WAAW,EACZ,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrD,MAAM,MAAM,QAAQ,GAChB,WAAW,GACX,WAAW,GACX,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,eAAe,GACf,UAAU,GACV,UAAU,GACV,WAAW,CAAC;AAEhB;;GAEG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,QAAQ,CAMxD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,QAAQ,EAAE,EACf,OAAO,GAAE,eAAoB,GAC5B,QAAQ,EAAE,CAkCZ"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-Write-Wins (LWW) Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Generic coalescer for all *.set operations.
|
|
5
|
+
* Set operations follow Last-Write-Wins semantics - only the last value matters.
|
|
6
|
+
*
|
|
7
|
+
* Supported operations:
|
|
8
|
+
* - number.set, string.set, boolean.set
|
|
9
|
+
* - vector3.set, euler.set, quaternion.set, color.set
|
|
10
|
+
* - array.set, object.set
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Check if an operation is a *.set operation
|
|
14
|
+
*/
|
|
15
|
+
export function isLWWSetOp(op) {
|
|
16
|
+
return (typeof op.otype === 'string' &&
|
|
17
|
+
op.otype.endsWith('.set') &&
|
|
18
|
+
op.value !== undefined);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Coalesce consecutive *.set operations using Last-Write-Wins semantics.
|
|
22
|
+
*
|
|
23
|
+
* Since set operations follow LWW, only the last value in a sequence matters.
|
|
24
|
+
* We can safely discard all but the last operation for the same target.
|
|
25
|
+
*
|
|
26
|
+
* Example:
|
|
27
|
+
* Input: [set(key:"cube", path:"color", value:"red"), set(key:"cube", path:"color", value:"blue")]
|
|
28
|
+
* Output: [set(key:"cube", path:"color", value:"blue")]
|
|
29
|
+
*
|
|
30
|
+
* @param ops - Array of *.set operations
|
|
31
|
+
* @param options - Coalescence options
|
|
32
|
+
* NOTE: Time threshold is ignored for Last-Write-Wins operations.
|
|
33
|
+
* LWW semantics mean only the final value matters, regardless of timing,
|
|
34
|
+
* so we keep only the last operation on each target.
|
|
35
|
+
* @returns New array with coalesced operations
|
|
36
|
+
*/
|
|
37
|
+
export function coalesceLWWSets(ops, options = {}) {
|
|
38
|
+
if (ops.length === 0)
|
|
39
|
+
return ops;
|
|
40
|
+
const result = [];
|
|
41
|
+
let pending = null;
|
|
42
|
+
for (const op of ops) {
|
|
43
|
+
if (pending === null) {
|
|
44
|
+
// Start new pending set
|
|
45
|
+
pending = { ...op };
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Check if operations target the same property AND same operation type
|
|
49
|
+
const sameTarget = pending.key === op.key &&
|
|
50
|
+
pending.path === op.path &&
|
|
51
|
+
pending.otype === op.otype;
|
|
52
|
+
if (sameTarget) {
|
|
53
|
+
// LWW: Replace with newer value
|
|
54
|
+
pending = { ...op };
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Different target or type - flush pending and start new
|
|
58
|
+
result.push(pending);
|
|
59
|
+
pending = { ...op };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Flush any remaining pending set
|
|
64
|
+
if (pending !== null) {
|
|
65
|
+
result.push(pending);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=lwwOperations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lwwOperations.js","sourceRoot":"","sources":["../../../src/client/coalescence/lwwOperations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA4BH;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,EAAa;IACtC,OAAO,CACL,OAAO,EAAE,CAAC,KAAK,KAAK,QAAQ;QAC5B,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QACxB,EAAU,CAAC,KAAK,KAAK,SAAS,CAChC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAe,EACf,UAA2B,EAAE;IAE7B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEjC,MAAM,MAAM,GAAe,EAAE,CAAC;IAC9B,IAAI,OAAO,GAAoB,IAAI,CAAC;IAEpC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,wBAAwB;YACxB,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,uEAAuE;YACvE,MAAM,UAAU,GACd,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG;gBACtB,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI;gBACxB,OAAO,CAAC,KAAK,KAAK,EAAE,CAAC,KAAK,CAAC;YAE7B,IAAI,UAAU,EAAE,CAAC;gBACf,gCAAgC;gBAChC,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;YACtB,CAAC;iBAAM,CAAC;gBACN,yDAAyD;gBACzD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Merges consecutive number.add operations on the same target.
|
|
5
|
+
* Addition is commutative, so we can safely merge multiple adds into one.
|
|
6
|
+
*/
|
|
7
|
+
import type { Operation, NumberAddOp } from '../../operations/OperationTypes.js';
|
|
8
|
+
import type { CoalesceOptions } from './registry.js';
|
|
9
|
+
export type { NumberAddOp };
|
|
10
|
+
/**
|
|
11
|
+
* Check if an operation is a number.add operation
|
|
12
|
+
*/
|
|
13
|
+
export declare function isNumberAddOp(op: Operation): op is NumberAddOp;
|
|
14
|
+
/**
|
|
15
|
+
* Coalesce consecutive number.add operations.
|
|
16
|
+
*
|
|
17
|
+
* Since addition is commutative (a + b = b + a), we can safely merge
|
|
18
|
+
* all consecutive adds into a single operation by summing the values.
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
* Input: [add(key:"score", path:"value", value:10), add(key:"score", path:"value", value:5)]
|
|
22
|
+
* Output: [add(key:"score", path:"value", value:15)]
|
|
23
|
+
*
|
|
24
|
+
* @param ops - Array of number.add operations
|
|
25
|
+
* @param options - Coalescence options
|
|
26
|
+
* NOTE: Time threshold is ignored for commutative operations like addition.
|
|
27
|
+
* Mathematically, a+b = b+a regardless of when they occurred, so we merge
|
|
28
|
+
* all consecutive operations on the same target without time restrictions.
|
|
29
|
+
* @returns New array with coalesced operations
|
|
30
|
+
*/
|
|
31
|
+
export declare function coalesceNumberAdds(ops: NumberAddOp[], options?: CoalesceOptions): NumberAddOp[];
|
|
32
|
+
//# sourceMappingURL=numberOperations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numberOperations.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/numberOperations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrD,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,WAAW,CAK9D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,WAAW,EAAE,EAClB,OAAO,GAAE,eAAoB,GAC5B,WAAW,EAAE,CAoCf"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Merges consecutive number.add operations on the same target.
|
|
5
|
+
* Addition is commutative, so we can safely merge multiple adds into one.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if an operation is a number.add operation
|
|
9
|
+
*/
|
|
10
|
+
export function isNumberAddOp(op) {
|
|
11
|
+
return (op.otype === 'number.add' &&
|
|
12
|
+
typeof op.value === 'number');
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Coalesce consecutive number.add operations.
|
|
16
|
+
*
|
|
17
|
+
* Since addition is commutative (a + b = b + a), we can safely merge
|
|
18
|
+
* all consecutive adds into a single operation by summing the values.
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
* Input: [add(key:"score", path:"value", value:10), add(key:"score", path:"value", value:5)]
|
|
22
|
+
* Output: [add(key:"score", path:"value", value:15)]
|
|
23
|
+
*
|
|
24
|
+
* @param ops - Array of number.add operations
|
|
25
|
+
* @param options - Coalescence options
|
|
26
|
+
* NOTE: Time threshold is ignored for commutative operations like addition.
|
|
27
|
+
* Mathematically, a+b = b+a regardless of when they occurred, so we merge
|
|
28
|
+
* all consecutive operations on the same target without time restrictions.
|
|
29
|
+
* @returns New array with coalesced operations
|
|
30
|
+
*/
|
|
31
|
+
export function coalesceNumberAdds(ops, options = {}) {
|
|
32
|
+
if (ops.length === 0)
|
|
33
|
+
return ops;
|
|
34
|
+
const result = [];
|
|
35
|
+
let pending = null;
|
|
36
|
+
for (const op of ops) {
|
|
37
|
+
if (pending === null) {
|
|
38
|
+
// Start new pending add
|
|
39
|
+
pending = { ...op };
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Check if operations target the same property
|
|
43
|
+
const sameTarget = pending.key === op.key && pending.path === op.path;
|
|
44
|
+
if (sameTarget) {
|
|
45
|
+
// Merge operations by summing values
|
|
46
|
+
pending = {
|
|
47
|
+
otype: 'number.add',
|
|
48
|
+
key: pending.key,
|
|
49
|
+
path: pending.path,
|
|
50
|
+
value: pending.value + op.value,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Different target - flush pending and start new
|
|
55
|
+
result.push(pending);
|
|
56
|
+
pending = { ...op };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Flush any remaining pending add
|
|
61
|
+
if (pending !== null) {
|
|
62
|
+
result.push(pending);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=numberOperations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numberOperations.js","sourceRoot":"","sources":["../../../src/client/coalescence/numberOperations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,EAAa;IACzC,OAAO,CACL,EAAE,CAAC,KAAK,KAAK,YAAY;QACzB,OAAQ,EAAU,CAAC,KAAK,KAAK,QAAQ,CACtC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAkB,EAClB,UAA2B,EAAE;IAE7B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEjC,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,IAAI,OAAO,GAAuB,IAAI,CAAC;IAEvC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,wBAAwB;YACxB,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;QACtB,CAAC;aAAM,CAAC;YACN,+CAA+C;YAC/C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,CAAC;YAEtE,IAAI,UAAU,EAAE,CAAC;gBACf,qCAAqC;gBACrC,OAAO,GAAG;oBACR,KAAK,EAAE,YAAY;oBACnB,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,KAAK,EAAE,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,KAAK;iBAChC,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,iDAAiD;gBACjD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAOpE,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,IAAI,CAC7D,GAAG,EAAE,CAAC,EAAE,EACR,OAAO,EAAE,eAAe,KACrB,CAAC,EAAE,CAAC;AAET;;GAEG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,IAAI,CAAC,EAAE,EAAE,SAAS,KAAK,EAAE,IAAI,CAAC,CAAC;AAEpF;;GAEG;AACH,UAAU,aAAa,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS;IACrD,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;CAC7B;AAOD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,SAAS,EACnD,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EACnB,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAC1B,IAAI,CAKN;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAErE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,SAAS,EAAE,EAChB,OAAO,GAAE,eAAoB,GAC5B,SAAS,EAAE,CAoDb"}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { coalesceTextInserts, isTextInsertOp } from './textInserts.js';
|
|
8
8
|
import { coalesceTextDeletes, isTextDeleteOp } from './textDeletes.js';
|
|
9
|
+
import { coalesceNumberAdds, isNumberAddOp } from './numberOperations.js';
|
|
10
|
+
import { coalesceVector3Adds, isVector3AddOp } from './vector3Operations.js';
|
|
11
|
+
import { coalesceLWWSets, isLWWSetOp } from './lwwOperations.js';
|
|
9
12
|
/**
|
|
10
13
|
* Global coalescence registry
|
|
11
14
|
*/
|
|
@@ -92,4 +95,21 @@ export function coalesceOperations(ops, options = {}) {
|
|
|
92
95
|
// Register built-in coalescers
|
|
93
96
|
registerCoalescer('text.insert', isTextInsertOp, coalesceTextInserts);
|
|
94
97
|
registerCoalescer('text.delete', isTextDeleteOp, coalesceTextDeletes);
|
|
98
|
+
registerCoalescer('number.add', isNumberAddOp, coalesceNumberAdds);
|
|
99
|
+
registerCoalescer('vector3.add', isVector3AddOp, coalesceVector3Adds);
|
|
100
|
+
// Register LWW coalescer for all *.set operations
|
|
101
|
+
const LWW_SET_OPERATIONS = [
|
|
102
|
+
'number.set',
|
|
103
|
+
'string.set',
|
|
104
|
+
'boolean.set',
|
|
105
|
+
'vector3.set',
|
|
106
|
+
'euler.set',
|
|
107
|
+
'quaternion.set',
|
|
108
|
+
'color.set',
|
|
109
|
+
'array.set',
|
|
110
|
+
'object.set',
|
|
111
|
+
];
|
|
112
|
+
for (const otype of LWW_SET_OPERATIONS) {
|
|
113
|
+
registerCoalescer(otype, isLWWSetOp, coalesceLWWSets);
|
|
114
|
+
}
|
|
95
115
|
//# sourceMappingURL=registry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/client/coalescence/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../src/client/coalescence/registry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAqB,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AA4BjE;;GAEG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;AAElD;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAa,EACb,KAAmB,EACnB,OAA2B;IAE3B,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE;QAClB,KAAK,EAAE,KAA6B;QACpC,OAAO,EAAE,OAAqC;KAC/C,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAgB,EAChB,UAA2B,EAAE;IAE7B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEjC,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,IAAI,YAAY,GAAgB,EAAE,CAAC;IACnC,IAAI,WAAW,GAAkB,IAAI,CAAC;IAEtC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QAErC,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7B,4BAA4B;YAC5B,IAAI,WAAW,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC;gBAC7B,mCAAmC;gBACnC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACxB,CAAC;iBAAM,CAAC;gBACN,qDAAqD;gBACrD,IAAI,WAAW,KAAK,IAAI,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACpD,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;oBAC5C,IAAI,SAAS,EAAE,CAAC;wBACd,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,YAAmB,EAAE,OAAO,CAAC,CAAC;wBAClE,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;oBAC5B,CAAC;gBACH,CAAC;gBACD,WAAW,GAAG,EAAE,CAAC,KAAK,CAAC;gBACvB,YAAY,GAAG,CAAC,EAAE,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;aAAM,CAAC;YACN,gDAAgD;YAChD,IAAI,WAAW,KAAK,IAAI,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpD,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBAC5C,IAAI,SAAS,EAAE,CAAC;oBACd,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,YAAmB,EAAE,OAAO,CAAC,CAAC;oBAClE,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;YACD,WAAW,GAAG,IAAI,CAAC;YACnB,YAAY,GAAG,EAAE,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;IACH,CAAC;IAED,oCAAoC;IACpC,IAAI,WAAW,KAAK,IAAI,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,YAAmB,EAAE,OAAO,CAAC,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,+BAA+B;AAC/B,iBAAiB,CAAC,aAAa,EAAE,cAAgC,EAAE,mBAA2C,CAAC,CAAC;AAChH,iBAAiB,CAAC,aAAa,EAAE,cAAgC,EAAE,mBAA2C,CAAC,CAAC;AAChH,iBAAiB,CAAC,YAAY,EAAE,aAA+B,EAAE,kBAA0C,CAAC,CAAC;AAC7G,iBAAiB,CAAC,aAAa,EAAE,cAAgC,EAAE,mBAA2C,CAAC,CAAC;AAEhH,kDAAkD;AAClD,MAAM,kBAAkB,GAAG;IACzB,YAAY;IACZ,YAAY;IACZ,aAAa;IACb,aAAa;IACb,WAAW;IACX,gBAAgB;IAChB,WAAW;IACX,WAAW;IACX,YAAY;CACJ,CAAC;AAEX,KAAK,MAAM,KAAK,IAAI,kBAAkB,EAAE,CAAC;IACvC,iBAAiB,CAAC,KAAK,EAAE,UAA4B,EAAE,eAAuC,CAAC,CAAC;AAClG,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector3 Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Merges consecutive vector3.add operations on the same target.
|
|
5
|
+
* Component-wise addition is commutative, so we can safely merge multiple adds.
|
|
6
|
+
*/
|
|
7
|
+
import type { Operation, Vector3AddOp } from '../../operations/OperationTypes.js';
|
|
8
|
+
import type { CoalesceOptions } from './registry.js';
|
|
9
|
+
export type { Vector3AddOp };
|
|
10
|
+
/**
|
|
11
|
+
* Check if an operation is a vector3.add operation
|
|
12
|
+
*/
|
|
13
|
+
export declare function isVector3AddOp(op: Operation): op is Vector3AddOp;
|
|
14
|
+
/**
|
|
15
|
+
* Coalesce consecutive vector3.add operations.
|
|
16
|
+
*
|
|
17
|
+
* Since component-wise addition is commutative, we can safely merge
|
|
18
|
+
* all consecutive adds into a single operation by summing the vectors.
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
* Input: [add(key:"player", path:"position", value:[1,0,0]), add(key:"player", path:"position", value:[0,2,0])]
|
|
22
|
+
* Output: [add(key:"player", path:"position", value:[1,2,0])]
|
|
23
|
+
*
|
|
24
|
+
* @param ops - Array of vector3.add operations
|
|
25
|
+
* @param options - Coalescence options
|
|
26
|
+
* NOTE: Time threshold is ignored for commutative operations like vector addition.
|
|
27
|
+
* Component-wise addition is commutative regardless of timing, so we merge
|
|
28
|
+
* all consecutive operations on the same target without time restrictions.
|
|
29
|
+
* @returns New array with coalesced operations
|
|
30
|
+
*/
|
|
31
|
+
export declare function coalesceVector3Adds(ops: Vector3AddOp[], options?: CoalesceOptions): Vector3AddOp[];
|
|
32
|
+
//# sourceMappingURL=vector3Operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector3Operations.d.ts","sourceRoot":"","sources":["../../../src/client/coalescence/vector3Operations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAClF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGrD,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,YAAY,CAMhE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,YAAY,EAAE,EACnB,OAAO,GAAE,eAAoB,GAC5B,YAAY,EAAE,CAwChB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector3 Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Merges consecutive vector3.add operations on the same target.
|
|
5
|
+
* Component-wise addition is commutative, so we can safely merge multiple adds.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if an operation is a vector3.add operation
|
|
9
|
+
*/
|
|
10
|
+
export function isVector3AddOp(op) {
|
|
11
|
+
return (op.otype === 'vector3.add' &&
|
|
12
|
+
Array.isArray(op.value) &&
|
|
13
|
+
op.value.length === 3);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Coalesce consecutive vector3.add operations.
|
|
17
|
+
*
|
|
18
|
+
* Since component-wise addition is commutative, we can safely merge
|
|
19
|
+
* all consecutive adds into a single operation by summing the vectors.
|
|
20
|
+
*
|
|
21
|
+
* Example:
|
|
22
|
+
* Input: [add(key:"player", path:"position", value:[1,0,0]), add(key:"player", path:"position", value:[0,2,0])]
|
|
23
|
+
* Output: [add(key:"player", path:"position", value:[1,2,0])]
|
|
24
|
+
*
|
|
25
|
+
* @param ops - Array of vector3.add operations
|
|
26
|
+
* @param options - Coalescence options
|
|
27
|
+
* NOTE: Time threshold is ignored for commutative operations like vector addition.
|
|
28
|
+
* Component-wise addition is commutative regardless of timing, so we merge
|
|
29
|
+
* all consecutive operations on the same target without time restrictions.
|
|
30
|
+
* @returns New array with coalesced operations
|
|
31
|
+
*/
|
|
32
|
+
export function coalesceVector3Adds(ops, options = {}) {
|
|
33
|
+
if (ops.length === 0)
|
|
34
|
+
return ops;
|
|
35
|
+
const result = [];
|
|
36
|
+
let pending = null;
|
|
37
|
+
for (const op of ops) {
|
|
38
|
+
if (pending === null) {
|
|
39
|
+
// Start new pending add (copy value array to avoid mutations)
|
|
40
|
+
pending = { ...op, value: [...op.value] };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// Check if operations target the same property
|
|
44
|
+
const sameTarget = pending.key === op.key && pending.path === op.path;
|
|
45
|
+
if (sameTarget) {
|
|
46
|
+
// Merge operations by summing vectors component-wise
|
|
47
|
+
pending = {
|
|
48
|
+
otype: 'vector3.add',
|
|
49
|
+
key: pending.key,
|
|
50
|
+
path: pending.path,
|
|
51
|
+
value: [
|
|
52
|
+
pending.value[0] + op.value[0],
|
|
53
|
+
pending.value[1] + op.value[1],
|
|
54
|
+
pending.value[2] + op.value[2],
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Different target - flush pending and start new
|
|
60
|
+
result.push(pending);
|
|
61
|
+
pending = { ...op, value: [...op.value] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Flush any remaining pending add
|
|
66
|
+
if (pending !== null) {
|
|
67
|
+
result.push(pending);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=vector3Operations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector3Operations.js","sourceRoot":"","sources":["../../../src/client/coalescence/vector3Operations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,EAAa;IAC1C,OAAO,CACL,EAAE,CAAC,KAAK,KAAK,aAAa;QAC1B,KAAK,CAAC,OAAO,CAAE,EAAU,CAAC,KAAK,CAAC;QAC/B,EAAU,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,CAC/B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAmB,EACnB,UAA2B,EAAE;IAE7B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEjC,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,IAAI,OAAO,GAAwB,IAAI,CAAC;IAExC,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;QACrB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,8DAA8D;YAC9D,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAA6B,EAAE,CAAC;QACxE,CAAC;aAAM,CAAC;YACN,+CAA+C;YAC/C,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,CAAC;YAEtE,IAAI,UAAU,EAAE,CAAC;gBACf,qDAAqD;gBACrD,OAAO,GAAG;oBACR,KAAK,EAAE,aAAa;oBACpB,GAAG,EAAE,OAAO,CAAC,GAAG;oBAChB,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,KAAK,EAAE;wBACL,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;wBAC9B,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;wBAC9B,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;qBAC/B;iBACF,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,iDAAiD;gBACjD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAA6B,EAAE,CAAC;YACxE,CAAC;QACH,CAAC;IACH,CAAC;IAED,kCAAkC;IAClC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -16,3 +16,6 @@ export {
|
|
|
16
16
|
|
|
17
17
|
export { coalesceTextInserts, isTextInsertOp, type TextInsertOp } from './textInserts.js';
|
|
18
18
|
export { coalesceTextDeletes, isTextDeleteOp, type TextDeleteOp } from './textDeletes.js';
|
|
19
|
+
export { coalesceNumberAdds, isNumberAddOp, type NumberAddOp } from './numberOperations.js';
|
|
20
|
+
export { coalesceVector3Adds, isVector3AddOp, type Vector3AddOp } from './vector3Operations.js';
|
|
21
|
+
export { coalesceLWWSets, isLWWSetOp, type LWWSetOp } from './lwwOperations.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Last-Write-Wins (LWW) Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Generic coalescer for all *.set operations.
|
|
5
|
+
* Set operations follow Last-Write-Wins semantics - only the last value matters.
|
|
6
|
+
*
|
|
7
|
+
* Supported operations:
|
|
8
|
+
* - number.set, string.set, boolean.set
|
|
9
|
+
* - vector3.set, euler.set, quaternion.set, color.set
|
|
10
|
+
* - array.set, object.set
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
Operation,
|
|
15
|
+
NumberSetOp,
|
|
16
|
+
StringSetOp,
|
|
17
|
+
BooleanSetOp,
|
|
18
|
+
Vector3SetOp,
|
|
19
|
+
EulerSetOp,
|
|
20
|
+
QuaternionSetOp,
|
|
21
|
+
ColorSetOp,
|
|
22
|
+
ArraySetOp,
|
|
23
|
+
ObjectSetOp,
|
|
24
|
+
} from '../../operations/OperationTypes.js';
|
|
25
|
+
import type { CoalesceOptions } from './registry.js';
|
|
26
|
+
|
|
27
|
+
// Union of all *.set operation types
|
|
28
|
+
export type LWWSetOp =
|
|
29
|
+
| NumberSetOp
|
|
30
|
+
| StringSetOp
|
|
31
|
+
| BooleanSetOp
|
|
32
|
+
| Vector3SetOp
|
|
33
|
+
| EulerSetOp
|
|
34
|
+
| QuaternionSetOp
|
|
35
|
+
| ColorSetOp
|
|
36
|
+
| ArraySetOp
|
|
37
|
+
| ObjectSetOp;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if an operation is a *.set operation
|
|
41
|
+
*/
|
|
42
|
+
export function isLWWSetOp(op: Operation): op is LWWSetOp {
|
|
43
|
+
return (
|
|
44
|
+
typeof op.otype === 'string' &&
|
|
45
|
+
op.otype.endsWith('.set') &&
|
|
46
|
+
(op as any).value !== undefined
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Coalesce consecutive *.set operations using Last-Write-Wins semantics.
|
|
52
|
+
*
|
|
53
|
+
* Since set operations follow LWW, only the last value in a sequence matters.
|
|
54
|
+
* We can safely discard all but the last operation for the same target.
|
|
55
|
+
*
|
|
56
|
+
* Example:
|
|
57
|
+
* Input: [set(key:"cube", path:"color", value:"red"), set(key:"cube", path:"color", value:"blue")]
|
|
58
|
+
* Output: [set(key:"cube", path:"color", value:"blue")]
|
|
59
|
+
*
|
|
60
|
+
* @param ops - Array of *.set operations
|
|
61
|
+
* @param options - Coalescence options
|
|
62
|
+
* NOTE: Time threshold is ignored for Last-Write-Wins operations.
|
|
63
|
+
* LWW semantics mean only the final value matters, regardless of timing,
|
|
64
|
+
* so we keep only the last operation on each target.
|
|
65
|
+
* @returns New array with coalesced operations
|
|
66
|
+
*/
|
|
67
|
+
export function coalesceLWWSets(
|
|
68
|
+
ops: LWWSetOp[],
|
|
69
|
+
options: CoalesceOptions = {}
|
|
70
|
+
): LWWSetOp[] {
|
|
71
|
+
if (ops.length === 0) return ops;
|
|
72
|
+
|
|
73
|
+
const result: LWWSetOp[] = [];
|
|
74
|
+
let pending: LWWSetOp | null = null;
|
|
75
|
+
|
|
76
|
+
for (const op of ops) {
|
|
77
|
+
if (pending === null) {
|
|
78
|
+
// Start new pending set
|
|
79
|
+
pending = { ...op };
|
|
80
|
+
} else {
|
|
81
|
+
// Check if operations target the same property AND same operation type
|
|
82
|
+
const sameTarget =
|
|
83
|
+
pending.key === op.key &&
|
|
84
|
+
pending.path === op.path &&
|
|
85
|
+
pending.otype === op.otype;
|
|
86
|
+
|
|
87
|
+
if (sameTarget) {
|
|
88
|
+
// LWW: Replace with newer value
|
|
89
|
+
pending = { ...op };
|
|
90
|
+
} else {
|
|
91
|
+
// Different target or type - flush pending and start new
|
|
92
|
+
result.push(pending);
|
|
93
|
+
pending = { ...op };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Flush any remaining pending set
|
|
99
|
+
if (pending !== null) {
|
|
100
|
+
result.push(pending);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Merges consecutive number.add operations on the same target.
|
|
5
|
+
* Addition is commutative, so we can safely merge multiple adds into one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Operation, NumberAddOp } from '../../operations/OperationTypes.js';
|
|
9
|
+
import type { CoalesceOptions } from './registry.js';
|
|
10
|
+
|
|
11
|
+
// Re-export type for convenience
|
|
12
|
+
export type { NumberAddOp };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if an operation is a number.add operation
|
|
16
|
+
*/
|
|
17
|
+
export function isNumberAddOp(op: Operation): op is NumberAddOp {
|
|
18
|
+
return (
|
|
19
|
+
op.otype === 'number.add' &&
|
|
20
|
+
typeof (op as any).value === 'number'
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Coalesce consecutive number.add operations.
|
|
26
|
+
*
|
|
27
|
+
* Since addition is commutative (a + b = b + a), we can safely merge
|
|
28
|
+
* all consecutive adds into a single operation by summing the values.
|
|
29
|
+
*
|
|
30
|
+
* Example:
|
|
31
|
+
* Input: [add(key:"score", path:"value", value:10), add(key:"score", path:"value", value:5)]
|
|
32
|
+
* Output: [add(key:"score", path:"value", value:15)]
|
|
33
|
+
*
|
|
34
|
+
* @param ops - Array of number.add operations
|
|
35
|
+
* @param options - Coalescence options
|
|
36
|
+
* NOTE: Time threshold is ignored for commutative operations like addition.
|
|
37
|
+
* Mathematically, a+b = b+a regardless of when they occurred, so we merge
|
|
38
|
+
* all consecutive operations on the same target without time restrictions.
|
|
39
|
+
* @returns New array with coalesced operations
|
|
40
|
+
*/
|
|
41
|
+
export function coalesceNumberAdds(
|
|
42
|
+
ops: NumberAddOp[],
|
|
43
|
+
options: CoalesceOptions = {}
|
|
44
|
+
): NumberAddOp[] {
|
|
45
|
+
if (ops.length === 0) return ops;
|
|
46
|
+
|
|
47
|
+
const result: NumberAddOp[] = [];
|
|
48
|
+
let pending: NumberAddOp | null = null;
|
|
49
|
+
|
|
50
|
+
for (const op of ops) {
|
|
51
|
+
if (pending === null) {
|
|
52
|
+
// Start new pending add
|
|
53
|
+
pending = { ...op };
|
|
54
|
+
} else {
|
|
55
|
+
// Check if operations target the same property
|
|
56
|
+
const sameTarget = pending.key === op.key && pending.path === op.path;
|
|
57
|
+
|
|
58
|
+
if (sameTarget) {
|
|
59
|
+
// Merge operations by summing values
|
|
60
|
+
pending = {
|
|
61
|
+
otype: 'number.add',
|
|
62
|
+
key: pending.key,
|
|
63
|
+
path: pending.path,
|
|
64
|
+
value: pending.value + op.value,
|
|
65
|
+
};
|
|
66
|
+
} else {
|
|
67
|
+
// Different target - flush pending and start new
|
|
68
|
+
result.push(pending);
|
|
69
|
+
pending = { ...op };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Flush any remaining pending add
|
|
75
|
+
if (pending !== null) {
|
|
76
|
+
result.push(pending);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
import type { Operation } from '../../operations/OperationTypes.js';
|
|
9
9
|
import { coalesceTextInserts, isTextInsertOp, type TextInsertOp } from './textInserts.js';
|
|
10
10
|
import { coalesceTextDeletes, isTextDeleteOp, type TextDeleteOp } from './textDeletes.js';
|
|
11
|
+
import { coalesceNumberAdds, isNumberAddOp } from './numberOperations.js';
|
|
12
|
+
import { coalesceVector3Adds, isVector3AddOp } from './vector3Operations.js';
|
|
13
|
+
import { coalesceLWWSets, isLWWSetOp } from './lwwOperations.js';
|
|
11
14
|
|
|
12
15
|
export interface CoalesceOptions {
|
|
13
16
|
/** Time threshold in milliseconds (default: 1000ms = 1 second) */
|
|
@@ -135,3 +138,22 @@ export function coalesceOperations(
|
|
|
135
138
|
// Register built-in coalescers
|
|
136
139
|
registerCoalescer('text.insert', isTextInsertOp as TypeGuard<any>, coalesceTextInserts as CoalesceHandler<any>);
|
|
137
140
|
registerCoalescer('text.delete', isTextDeleteOp as TypeGuard<any>, coalesceTextDeletes as CoalesceHandler<any>);
|
|
141
|
+
registerCoalescer('number.add', isNumberAddOp as TypeGuard<any>, coalesceNumberAdds as CoalesceHandler<any>);
|
|
142
|
+
registerCoalescer('vector3.add', isVector3AddOp as TypeGuard<any>, coalesceVector3Adds as CoalesceHandler<any>);
|
|
143
|
+
|
|
144
|
+
// Register LWW coalescer for all *.set operations
|
|
145
|
+
const LWW_SET_OPERATIONS = [
|
|
146
|
+
'number.set',
|
|
147
|
+
'string.set',
|
|
148
|
+
'boolean.set',
|
|
149
|
+
'vector3.set',
|
|
150
|
+
'euler.set',
|
|
151
|
+
'quaternion.set',
|
|
152
|
+
'color.set',
|
|
153
|
+
'array.set',
|
|
154
|
+
'object.set',
|
|
155
|
+
] as const;
|
|
156
|
+
|
|
157
|
+
for (const otype of LWW_SET_OPERATIONS) {
|
|
158
|
+
registerCoalescer(otype, isLWWSetOp as TypeGuard<any>, coalesceLWWSets as CoalesceHandler<any>);
|
|
159
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vector3 Operations Coalescence
|
|
3
|
+
*
|
|
4
|
+
* Merges consecutive vector3.add operations on the same target.
|
|
5
|
+
* Component-wise addition is commutative, so we can safely merge multiple adds.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Operation, Vector3AddOp } from '../../operations/OperationTypes.js';
|
|
9
|
+
import type { CoalesceOptions } from './registry.js';
|
|
10
|
+
|
|
11
|
+
// Re-export type for convenience
|
|
12
|
+
export type { Vector3AddOp };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if an operation is a vector3.add operation
|
|
16
|
+
*/
|
|
17
|
+
export function isVector3AddOp(op: Operation): op is Vector3AddOp {
|
|
18
|
+
return (
|
|
19
|
+
op.otype === 'vector3.add' &&
|
|
20
|
+
Array.isArray((op as any).value) &&
|
|
21
|
+
(op as any).value.length === 3
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Coalesce consecutive vector3.add operations.
|
|
27
|
+
*
|
|
28
|
+
* Since component-wise addition is commutative, we can safely merge
|
|
29
|
+
* all consecutive adds into a single operation by summing the vectors.
|
|
30
|
+
*
|
|
31
|
+
* Example:
|
|
32
|
+
* Input: [add(key:"player", path:"position", value:[1,0,0]), add(key:"player", path:"position", value:[0,2,0])]
|
|
33
|
+
* Output: [add(key:"player", path:"position", value:[1,2,0])]
|
|
34
|
+
*
|
|
35
|
+
* @param ops - Array of vector3.add operations
|
|
36
|
+
* @param options - Coalescence options
|
|
37
|
+
* NOTE: Time threshold is ignored for commutative operations like vector addition.
|
|
38
|
+
* Component-wise addition is commutative regardless of timing, so we merge
|
|
39
|
+
* all consecutive operations on the same target without time restrictions.
|
|
40
|
+
* @returns New array with coalesced operations
|
|
41
|
+
*/
|
|
42
|
+
export function coalesceVector3Adds(
|
|
43
|
+
ops: Vector3AddOp[],
|
|
44
|
+
options: CoalesceOptions = {}
|
|
45
|
+
): Vector3AddOp[] {
|
|
46
|
+
if (ops.length === 0) return ops;
|
|
47
|
+
|
|
48
|
+
const result: Vector3AddOp[] = [];
|
|
49
|
+
let pending: Vector3AddOp | null = null;
|
|
50
|
+
|
|
51
|
+
for (const op of ops) {
|
|
52
|
+
if (pending === null) {
|
|
53
|
+
// Start new pending add (copy value array to avoid mutations)
|
|
54
|
+
pending = { ...op, value: [...op.value] as [number, number, number] };
|
|
55
|
+
} else {
|
|
56
|
+
// Check if operations target the same property
|
|
57
|
+
const sameTarget = pending.key === op.key && pending.path === op.path;
|
|
58
|
+
|
|
59
|
+
if (sameTarget) {
|
|
60
|
+
// Merge operations by summing vectors component-wise
|
|
61
|
+
pending = {
|
|
62
|
+
otype: 'vector3.add',
|
|
63
|
+
key: pending.key,
|
|
64
|
+
path: pending.path,
|
|
65
|
+
value: [
|
|
66
|
+
pending.value[0] + op.value[0],
|
|
67
|
+
pending.value[1] + op.value[1],
|
|
68
|
+
pending.value[2] + op.value[2],
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
// Different target - flush pending and start new
|
|
73
|
+
result.push(pending);
|
|
74
|
+
pending = { ...op, value: [...op.value] as [number, number, number] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Flush any remaining pending add
|
|
80
|
+
if (pending !== null) {
|
|
81
|
+
result.push(pending);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 1 Graph Operation Coalescence Tests
|
|
3
|
+
*
|
|
4
|
+
* Integration tests for:
|
|
5
|
+
* - number.add coalescence
|
|
6
|
+
* - vector3.add coalescence
|
|
7
|
+
* - LWW (*.set) coalescence
|
|
8
|
+
*
|
|
9
|
+
* Tests the full pipeline: GraphStore.edit → commit → coalescence → onSend
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
13
|
+
import { createGraph } from '../../src/client/createGraph.js';
|
|
14
|
+
import type { CRDTMessage } from '../../src/operations/OperationTypes.js';
|
|
15
|
+
|
|
16
|
+
describe('Phase 1 Graph Coalescence', () => {
|
|
17
|
+
let messages: CRDTMessage[];
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
messages = [];
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('number.add coalescence', () => {
|
|
24
|
+
it('should merge consecutive number.add operations on same target', () => {
|
|
25
|
+
const store = createGraph({
|
|
26
|
+
sessionId: 'alice',
|
|
27
|
+
coalescingEnabled: true,
|
|
28
|
+
coalescingDelayMs: 0,
|
|
29
|
+
coalescingThresholdMs: 1000,
|
|
30
|
+
onSend: (msg) => messages.push(msg),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Create a score counter
|
|
34
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { score: 0 } });
|
|
35
|
+
store.commit('create player');
|
|
36
|
+
|
|
37
|
+
// Add to score 3 times rapidly
|
|
38
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'score', value: 10 });
|
|
39
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'score', value: 5 });
|
|
40
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'score', value: 3 });
|
|
41
|
+
store.commit('add to score');
|
|
42
|
+
|
|
43
|
+
// Should coalesce into 1 operation with sum: 10 + 5 + 3 = 18
|
|
44
|
+
const scoreMsg = messages[1];
|
|
45
|
+
expect(scoreMsg.ops).toHaveLength(1);
|
|
46
|
+
expect(scoreMsg.ops[0]).toMatchObject({
|
|
47
|
+
otype: 'number.add',
|
|
48
|
+
key: 'player',
|
|
49
|
+
path: 'score',
|
|
50
|
+
value: 18,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should NOT merge number.add operations on different targets', () => {
|
|
55
|
+
const store = createGraph({
|
|
56
|
+
sessionId: 'alice',
|
|
57
|
+
coalescingEnabled: true,
|
|
58
|
+
coalescingDelayMs: 0,
|
|
59
|
+
coalescingThresholdMs: 1000,
|
|
60
|
+
onSend: (msg) => messages.push(msg),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
store.edit({ otype: 'node.insert', key: 'player1', value: { score: 0 } });
|
|
64
|
+
store.edit({ otype: 'node.insert', key: 'player2', value: { score: 0 } });
|
|
65
|
+
store.commit('create players');
|
|
66
|
+
|
|
67
|
+
// Add to different players
|
|
68
|
+
store.edit({ otype: 'number.add', key: 'player1', path: 'score', value: 10 });
|
|
69
|
+
store.edit({ otype: 'number.add', key: 'player2', path: 'score', value: 5 });
|
|
70
|
+
store.commit('add to scores');
|
|
71
|
+
|
|
72
|
+
// Should NOT coalesce (different keys)
|
|
73
|
+
const scoreMsg = messages[1];
|
|
74
|
+
expect(scoreMsg.ops).toHaveLength(2);
|
|
75
|
+
expect(scoreMsg.ops[0]).toMatchObject({
|
|
76
|
+
otype: 'number.add',
|
|
77
|
+
key: 'player1',
|
|
78
|
+
value: 10,
|
|
79
|
+
});
|
|
80
|
+
expect(scoreMsg.ops[1]).toMatchObject({
|
|
81
|
+
otype: 'number.add',
|
|
82
|
+
key: 'player2',
|
|
83
|
+
value: 5,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should NOT merge number.add operations on different paths', () => {
|
|
88
|
+
const store = createGraph({
|
|
89
|
+
sessionId: 'alice',
|
|
90
|
+
coalescingEnabled: true,
|
|
91
|
+
coalescingDelayMs: 0,
|
|
92
|
+
coalescingThresholdMs: 1000,
|
|
93
|
+
onSend: (msg) => messages.push(msg),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { score: 0, health: 100 } });
|
|
97
|
+
store.commit('create player');
|
|
98
|
+
|
|
99
|
+
// Add to different properties
|
|
100
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'score', value: 10 });
|
|
101
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'health', value: -5 });
|
|
102
|
+
store.commit('update stats');
|
|
103
|
+
|
|
104
|
+
// Should NOT coalesce (different paths)
|
|
105
|
+
const statsMsg = messages[1];
|
|
106
|
+
expect(statsMsg.ops).toHaveLength(2);
|
|
107
|
+
expect(statsMsg.ops[0].path).toBe('score');
|
|
108
|
+
expect(statsMsg.ops[1].path).toBe('health');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('vector3.add coalescence', () => {
|
|
113
|
+
it('should merge consecutive vector3.add operations on same target', () => {
|
|
114
|
+
const store = createGraph({
|
|
115
|
+
sessionId: 'alice',
|
|
116
|
+
coalescingEnabled: true,
|
|
117
|
+
coalescingDelayMs: 0,
|
|
118
|
+
coalescingThresholdMs: 1000,
|
|
119
|
+
onSend: (msg) => messages.push(msg),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { position: [0, 0, 0] } });
|
|
123
|
+
store.commit('create player');
|
|
124
|
+
|
|
125
|
+
// Move player in multiple steps
|
|
126
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
127
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'position', value: [0, 2, 0] });
|
|
128
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'position', value: [0, 0, 3] });
|
|
129
|
+
store.commit('move player');
|
|
130
|
+
|
|
131
|
+
// Should coalesce into 1 operation with sum: [1+0+0, 0+2+0, 0+0+3] = [1, 2, 3]
|
|
132
|
+
const moveMsg = messages[1];
|
|
133
|
+
expect(moveMsg.ops).toHaveLength(1);
|
|
134
|
+
expect(moveMsg.ops[0]).toMatchObject({
|
|
135
|
+
otype: 'vector3.add',
|
|
136
|
+
key: 'player',
|
|
137
|
+
path: 'position',
|
|
138
|
+
value: [1, 2, 3],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should NOT merge vector3.add operations on different targets', () => {
|
|
143
|
+
const store = createGraph({
|
|
144
|
+
sessionId: 'alice',
|
|
145
|
+
coalescingEnabled: true,
|
|
146
|
+
coalescingDelayMs: 0,
|
|
147
|
+
coalescingThresholdMs: 1000,
|
|
148
|
+
onSend: (msg) => messages.push(msg),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { position: [0, 0, 0], velocity: [0, 0, 0] } });
|
|
152
|
+
store.commit('create player');
|
|
153
|
+
|
|
154
|
+
// Add to different properties
|
|
155
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
156
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'velocity', value: [0, 1, 0] });
|
|
157
|
+
store.commit('update player');
|
|
158
|
+
|
|
159
|
+
// Should NOT coalesce (different paths)
|
|
160
|
+
const updateMsg = messages[1];
|
|
161
|
+
expect(updateMsg.ops).toHaveLength(2);
|
|
162
|
+
expect(updateMsg.ops[0].path).toBe('position');
|
|
163
|
+
expect(updateMsg.ops[1].path).toBe('velocity');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('LWW (*.set) coalescence', () => {
|
|
168
|
+
it('should keep only last number.set operation on same target', () => {
|
|
169
|
+
const store = createGraph({
|
|
170
|
+
sessionId: 'alice',
|
|
171
|
+
coalescingEnabled: true,
|
|
172
|
+
coalescingDelayMs: 0,
|
|
173
|
+
coalescingThresholdMs: 1000,
|
|
174
|
+
onSend: (msg) => messages.push(msg),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { score: 0 } });
|
|
178
|
+
store.commit('create player');
|
|
179
|
+
|
|
180
|
+
// Set score multiple times
|
|
181
|
+
store.edit({ otype: 'number.set', key: 'player', path: 'score', value: 10 });
|
|
182
|
+
store.edit({ otype: 'number.set', key: 'player', path: 'score', value: 20 });
|
|
183
|
+
store.edit({ otype: 'number.set', key: 'player', path: 'score', value: 30 });
|
|
184
|
+
store.commit('set score');
|
|
185
|
+
|
|
186
|
+
// Should keep only last set (LWW)
|
|
187
|
+
const scoreMsg = messages[1];
|
|
188
|
+
expect(scoreMsg.ops).toHaveLength(1);
|
|
189
|
+
expect(scoreMsg.ops[0]).toMatchObject({
|
|
190
|
+
otype: 'number.set',
|
|
191
|
+
key: 'player',
|
|
192
|
+
path: 'score',
|
|
193
|
+
value: 30, // Only last value
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should keep only last string.set operation on same target', () => {
|
|
198
|
+
const store = createGraph({
|
|
199
|
+
sessionId: 'alice',
|
|
200
|
+
coalescingEnabled: true,
|
|
201
|
+
coalescingDelayMs: 0,
|
|
202
|
+
coalescingThresholdMs: 1000,
|
|
203
|
+
onSend: (msg) => messages.push(msg),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { name: '' } });
|
|
207
|
+
store.commit('create player');
|
|
208
|
+
|
|
209
|
+
// Set name multiple times
|
|
210
|
+
store.edit({ otype: 'string.set', key: 'player', path: 'name', value: 'Alice' });
|
|
211
|
+
store.edit({ otype: 'string.set', key: 'player', path: 'name', value: 'Bob' });
|
|
212
|
+
store.edit({ otype: 'string.set', key: 'player', path: 'name', value: 'Charlie' });
|
|
213
|
+
store.commit('set name');
|
|
214
|
+
|
|
215
|
+
// Should keep only last set (LWW)
|
|
216
|
+
const nameMsg = messages[1];
|
|
217
|
+
expect(nameMsg.ops).toHaveLength(1);
|
|
218
|
+
expect(nameMsg.ops[0]).toMatchObject({
|
|
219
|
+
otype: 'string.set',
|
|
220
|
+
key: 'player',
|
|
221
|
+
path: 'name',
|
|
222
|
+
value: 'Charlie', // Only last value
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should keep only last boolean.set operation on same target', () => {
|
|
227
|
+
const store = createGraph({
|
|
228
|
+
sessionId: 'alice',
|
|
229
|
+
coalescingEnabled: true,
|
|
230
|
+
coalescingDelayMs: 0,
|
|
231
|
+
coalescingThresholdMs: 1000,
|
|
232
|
+
onSend: (msg) => messages.push(msg),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { active: false } });
|
|
236
|
+
store.commit('create player');
|
|
237
|
+
|
|
238
|
+
// Toggle active multiple times
|
|
239
|
+
store.edit({ otype: 'boolean.set', key: 'player', path: 'active', value: true });
|
|
240
|
+
store.edit({ otype: 'boolean.set', key: 'player', path: 'active', value: false });
|
|
241
|
+
store.edit({ otype: 'boolean.set', key: 'player', path: 'active', value: true });
|
|
242
|
+
store.commit('toggle active');
|
|
243
|
+
|
|
244
|
+
// Should keep only last set (LWW)
|
|
245
|
+
const activeMsg = messages[1];
|
|
246
|
+
expect(activeMsg.ops).toHaveLength(1);
|
|
247
|
+
expect(activeMsg.ops[0]).toMatchObject({
|
|
248
|
+
otype: 'boolean.set',
|
|
249
|
+
key: 'player',
|
|
250
|
+
path: 'active',
|
|
251
|
+
value: true, // Only last value
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should keep only last vector3.set operation on same target', () => {
|
|
256
|
+
const store = createGraph({
|
|
257
|
+
sessionId: 'alice',
|
|
258
|
+
coalescingEnabled: true,
|
|
259
|
+
coalescingDelayMs: 0,
|
|
260
|
+
coalescingThresholdMs: 1000,
|
|
261
|
+
onSend: (msg) => messages.push(msg),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { position: [0, 0, 0] } });
|
|
265
|
+
store.commit('create player');
|
|
266
|
+
|
|
267
|
+
// Set position multiple times
|
|
268
|
+
store.edit({ otype: 'vector3.set', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
269
|
+
store.edit({ otype: 'vector3.set', key: 'player', path: 'position', value: [2, 0, 0] });
|
|
270
|
+
store.edit({ otype: 'vector3.set', key: 'player', path: 'position', value: [3, 0, 0] });
|
|
271
|
+
store.commit('set position');
|
|
272
|
+
|
|
273
|
+
// Should keep only last set (LWW)
|
|
274
|
+
const posMsg = messages[1];
|
|
275
|
+
expect(posMsg.ops).toHaveLength(1);
|
|
276
|
+
expect(posMsg.ops[0]).toMatchObject({
|
|
277
|
+
otype: 'vector3.set',
|
|
278
|
+
key: 'player',
|
|
279
|
+
path: 'position',
|
|
280
|
+
value: [3, 0, 0], // Only last value
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should NOT merge set operations on different paths', () => {
|
|
285
|
+
const store = createGraph({
|
|
286
|
+
sessionId: 'alice',
|
|
287
|
+
coalescingEnabled: true,
|
|
288
|
+
coalescingDelayMs: 0,
|
|
289
|
+
coalescingThresholdMs: 1000,
|
|
290
|
+
onSend: (msg) => messages.push(msg),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { score: 0, health: 100 } });
|
|
294
|
+
store.commit('create player');
|
|
295
|
+
|
|
296
|
+
// Set different properties
|
|
297
|
+
store.edit({ otype: 'number.set', key: 'player', path: 'score', value: 10 });
|
|
298
|
+
store.edit({ otype: 'number.set', key: 'player', path: 'health', value: 50 });
|
|
299
|
+
store.commit('set stats');
|
|
300
|
+
|
|
301
|
+
// Should NOT coalesce (different paths)
|
|
302
|
+
const statsMsg = messages[1];
|
|
303
|
+
expect(statsMsg.ops).toHaveLength(2);
|
|
304
|
+
expect(statsMsg.ops[0].path).toBe('score');
|
|
305
|
+
expect(statsMsg.ops[1].path).toBe('health');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('Mixed operations', () => {
|
|
310
|
+
it('should coalesce operations independently by type', () => {
|
|
311
|
+
const store = createGraph({
|
|
312
|
+
sessionId: 'alice',
|
|
313
|
+
coalescingEnabled: true,
|
|
314
|
+
coalescingDelayMs: 0,
|
|
315
|
+
coalescingThresholdMs: 1000,
|
|
316
|
+
onSend: (msg) => messages.push(msg),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
store.edit({ otype: 'node.insert', key: 'player', value: { score: 0, position: [0, 0, 0], name: '' } });
|
|
320
|
+
store.commit('create player');
|
|
321
|
+
|
|
322
|
+
// Mix of operations
|
|
323
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'score', value: 10 });
|
|
324
|
+
store.edit({ otype: 'number.add', key: 'player', path: 'score', value: 5 });
|
|
325
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
326
|
+
store.edit({ otype: 'vector3.add', key: 'player', path: 'position', value: [0, 2, 0] });
|
|
327
|
+
store.edit({ otype: 'string.set', key: 'player', path: 'name', value: 'Alice' });
|
|
328
|
+
store.edit({ otype: 'string.set', key: 'player', path: 'name', value: 'Bob' });
|
|
329
|
+
store.commit('update player');
|
|
330
|
+
|
|
331
|
+
// Should coalesce each type independently
|
|
332
|
+
const updateMsg = messages[1];
|
|
333
|
+
expect(updateMsg.ops).toHaveLength(3); // 1 number.add, 1 vector3.add, 1 string.set
|
|
334
|
+
|
|
335
|
+
// number.add should be coalesced: 10 + 5 = 15
|
|
336
|
+
expect(updateMsg.ops[0]).toMatchObject({
|
|
337
|
+
otype: 'number.add',
|
|
338
|
+
path: 'score',
|
|
339
|
+
value: 15,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// vector3.add should be coalesced: [1, 0, 0] + [0, 2, 0] = [1, 2, 0]
|
|
343
|
+
expect(updateMsg.ops[1]).toMatchObject({
|
|
344
|
+
otype: 'vector3.add',
|
|
345
|
+
path: 'position',
|
|
346
|
+
value: [1, 2, 0],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// string.set should keep last: 'Bob'
|
|
350
|
+
expect(updateMsg.ops[2]).toMatchObject({
|
|
351
|
+
otype: 'string.set',
|
|
352
|
+
path: 'name',
|
|
353
|
+
value: 'Bob',
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|