@vuer-ai/vuer-rtc-server 0.1.1
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/.env +1 -0
- package/PHASE1_SUMMARY.md +94 -0
- package/README.md +423 -0
- package/dist/broker/InMemoryBroker.d.ts +24 -0
- package/dist/broker/InMemoryBroker.d.ts.map +1 -0
- package/dist/broker/InMemoryBroker.js +65 -0
- package/dist/broker/InMemoryBroker.js.map +1 -0
- package/dist/broker/index.d.ts +3 -0
- package/dist/broker/index.d.ts.map +1 -0
- package/dist/broker/index.js +2 -0
- package/dist/broker/index.js.map +1 -0
- package/dist/broker/types.d.ts +47 -0
- package/dist/broker/types.d.ts.map +1 -0
- package/dist/broker/types.js +9 -0
- package/dist/broker/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/journal/JournalRepository.d.ts +39 -0
- package/dist/journal/JournalRepository.d.ts.map +1 -0
- package/dist/journal/JournalRepository.js +102 -0
- package/dist/journal/JournalRepository.js.map +1 -0
- package/dist/journal/JournalService.d.ts +69 -0
- package/dist/journal/JournalService.d.ts.map +1 -0
- package/dist/journal/JournalService.js +224 -0
- package/dist/journal/JournalService.js.map +1 -0
- package/dist/journal/index.d.ts +6 -0
- package/dist/journal/index.d.ts.map +1 -0
- package/dist/journal/index.js +6 -0
- package/dist/journal/index.js.map +1 -0
- package/dist/persistence/DocumentRepository.d.ts +22 -0
- package/dist/persistence/DocumentRepository.d.ts.map +1 -0
- package/dist/persistence/DocumentRepository.js +66 -0
- package/dist/persistence/DocumentRepository.js.map +1 -0
- package/dist/persistence/PrismaClient.d.ts +8 -0
- package/dist/persistence/PrismaClient.d.ts.map +1 -0
- package/dist/persistence/PrismaClient.js +21 -0
- package/dist/persistence/PrismaClient.js.map +1 -0
- package/dist/persistence/SessionRepository.d.ts +22 -0
- package/dist/persistence/SessionRepository.d.ts.map +1 -0
- package/dist/persistence/SessionRepository.js +103 -0
- package/dist/persistence/SessionRepository.js.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +7 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +211 -0
- package/dist/serve.js.map +1 -0
- package/dist/transport/RTCServer.d.ts +92 -0
- package/dist/transport/RTCServer.d.ts.map +1 -0
- package/dist/transport/RTCServer.js +273 -0
- package/dist/transport/RTCServer.js.map +1 -0
- package/dist/transport/index.d.ts +2 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/jest.config.js +36 -0
- package/package.json +56 -0
- package/prisma/schema.prisma +121 -0
- package/src/broker/InMemoryBroker.ts +81 -0
- package/src/broker/index.ts +2 -0
- package/src/broker/types.ts +60 -0
- package/src/index.ts +23 -0
- package/src/journal/JournalRepository.ts +119 -0
- package/src/journal/JournalService.ts +291 -0
- package/src/journal/index.ts +10 -0
- package/src/persistence/DocumentRepository.ts +76 -0
- package/src/persistence/PrismaClient.ts +24 -0
- package/src/persistence/SessionRepository.ts +114 -0
- package/src/persistence/index.ts +7 -0
- package/src/serve.ts +240 -0
- package/src/transport/RTCServer.ts +327 -0
- package/src/transport/index.ts +1 -0
- package/src/version.ts +1 -0
- package/tests/README.md +112 -0
- package/tests/demo.ts +555 -0
- package/tests/e2e/convergence.test.ts +221 -0
- package/tests/e2e/helpers/assertions.ts +158 -0
- package/tests/e2e/helpers/createTestServer.ts +220 -0
- package/tests/e2e/latency.test.ts +512 -0
- package/tests/e2e/packet-loss.test.ts +229 -0
- package/tests/e2e/relay.test.ts +255 -0
- package/tests/e2e/sync-perf.test.ts +365 -0
- package/tests/e2e/sync-reconciliation.test.ts +237 -0
- package/tests/e2e/text-sync.test.ts +199 -0
- package/tests/e2e/tombstone-convergence.test.ts +356 -0
- package/tests/fixtures/array-ops.jsonl +6 -0
- package/tests/fixtures/boolean-ops.jsonl +6 -0
- package/tests/fixtures/color-ops.jsonl +4 -0
- package/tests/fixtures/edit-buffer.jsonl +3 -0
- package/tests/fixtures/messages.jsonl +4 -0
- package/tests/fixtures/node-ops.jsonl +6 -0
- package/tests/fixtures/number-ops.jsonl +7 -0
- package/tests/fixtures/object-ops.jsonl +4 -0
- package/tests/fixtures/operations.jsonl +7 -0
- package/tests/fixtures/string-ops.jsonl +4 -0
- package/tests/fixtures/undo-redo.jsonl +3 -0
- package/tests/fixtures/vector-ops.jsonl +9 -0
- package/tests/integration/repositories.test.ts +320 -0
- package/tests/journal/journal-service.test.ts +185 -0
- package/tests/test-data/datatypes.ts +677 -0
- package/tests/test-data/operations-example.ts +306 -0
- package/tests/test-data/scene-example.ts +247 -0
- package/tests/unit/operations.test.ts +310 -0
- package/tests/unit/vectorClock.test.ts +281 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +8 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Type Definitions - Complete operation sets for all data types
|
|
3
|
+
*
|
|
4
|
+
* This file defines:
|
|
5
|
+
* 1. What operations each data type supports
|
|
6
|
+
* 2. How those operations work
|
|
7
|
+
* 3. Conflict resolution semantics
|
|
8
|
+
*
|
|
9
|
+
* See docs/TYPE_BEHAVIORS.md for detailed behavior descriptions.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// PRIMITIVE DATA TYPES
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Number Data Type
|
|
18
|
+
*
|
|
19
|
+
* dtype: 'number'
|
|
20
|
+
* Supported operations: set, add, multiply, min, max
|
|
21
|
+
*/
|
|
22
|
+
export const NumberType = {
|
|
23
|
+
dtype: 'number',
|
|
24
|
+
operations: {
|
|
25
|
+
/**
|
|
26
|
+
* SET - Replace value (LWW)
|
|
27
|
+
* Usage: Absolute values, IDs, measurements
|
|
28
|
+
*/
|
|
29
|
+
set: {
|
|
30
|
+
behavior: 'lww',
|
|
31
|
+
merge: (ops: Array<{ value: number; lamportTime: number }>) => {
|
|
32
|
+
// Last-Write-Wins by lamport time
|
|
33
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
34
|
+
return sorted[0].value;
|
|
35
|
+
},
|
|
36
|
+
example: {
|
|
37
|
+
ops: [
|
|
38
|
+
{ value: 42, lamportTime: 100 },
|
|
39
|
+
{ value: 99, lamportTime: 101 },
|
|
40
|
+
],
|
|
41
|
+
result: 99,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ADD - Accumulate values
|
|
47
|
+
* Usage: Counters, scores, deltas
|
|
48
|
+
*/
|
|
49
|
+
add: {
|
|
50
|
+
behavior: 'additive',
|
|
51
|
+
merge: (ops: Array<{ value: number; lamportTime: number }>) => {
|
|
52
|
+
// Sum all concurrent operations
|
|
53
|
+
return ops.reduce((sum, op) => sum + op.value, 0);
|
|
54
|
+
},
|
|
55
|
+
example: {
|
|
56
|
+
ops: [
|
|
57
|
+
{ value: 10, lamportTime: 100 },
|
|
58
|
+
{ value: 5, lamportTime: 101 },
|
|
59
|
+
],
|
|
60
|
+
result: 15,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* MULTIPLY - Multiply values
|
|
66
|
+
* Usage: Scale factors, percentages
|
|
67
|
+
*/
|
|
68
|
+
multiply: {
|
|
69
|
+
behavior: 'multiplicative',
|
|
70
|
+
merge: (ops: Array<{ value: number; lamportTime: number }>) => {
|
|
71
|
+
return ops.reduce((product, op) => product * op.value, 1);
|
|
72
|
+
},
|
|
73
|
+
example: {
|
|
74
|
+
ops: [
|
|
75
|
+
{ value: 2, lamportTime: 100 },
|
|
76
|
+
{ value: 3, lamportTime: 101 },
|
|
77
|
+
],
|
|
78
|
+
result: 6,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* MIN - Take minimum value
|
|
84
|
+
* Usage: Lower bounds, quality settings
|
|
85
|
+
*/
|
|
86
|
+
min: {
|
|
87
|
+
behavior: 'min',
|
|
88
|
+
merge: (ops: Array<{ value: number; lamportTime: number }>) => {
|
|
89
|
+
return Math.min(...ops.map((op) => op.value));
|
|
90
|
+
},
|
|
91
|
+
example: {
|
|
92
|
+
ops: [
|
|
93
|
+
{ value: 720, lamportTime: 100 },
|
|
94
|
+
{ value: 1080, lamportTime: 101 },
|
|
95
|
+
],
|
|
96
|
+
result: 720,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* MAX - Take maximum value
|
|
102
|
+
* Usage: Upper bounds, quality settings
|
|
103
|
+
*/
|
|
104
|
+
max: {
|
|
105
|
+
behavior: 'max',
|
|
106
|
+
merge: (ops: Array<{ value: number; lamportTime: number }>) => {
|
|
107
|
+
return Math.max(...ops.map((op) => op.value));
|
|
108
|
+
},
|
|
109
|
+
example: {
|
|
110
|
+
ops: [
|
|
111
|
+
{ value: 720, lamportTime: 100 },
|
|
112
|
+
{ value: 1080, lamportTime: 101 },
|
|
113
|
+
],
|
|
114
|
+
result: 1080,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* String Data Type
|
|
122
|
+
*
|
|
123
|
+
* dtype: 'string'
|
|
124
|
+
* Supported operations: set, concat
|
|
125
|
+
*/
|
|
126
|
+
export const StringType = {
|
|
127
|
+
dtype: 'string',
|
|
128
|
+
operations: {
|
|
129
|
+
/**
|
|
130
|
+
* SET - Replace value (LWW)
|
|
131
|
+
* Usage: Names, labels, single values
|
|
132
|
+
*/
|
|
133
|
+
set: {
|
|
134
|
+
behavior: 'lww',
|
|
135
|
+
merge: (ops: Array<{ value: string; lamportTime: number }>) => {
|
|
136
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
137
|
+
return sorted[0].value;
|
|
138
|
+
},
|
|
139
|
+
example: {
|
|
140
|
+
ops: [
|
|
141
|
+
{ value: 'Cube', lamportTime: 100 },
|
|
142
|
+
{ value: 'Sphere', lamportTime: 101 },
|
|
143
|
+
],
|
|
144
|
+
result: 'Sphere',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* CONCAT - Concatenate values
|
|
150
|
+
* Usage: Logs, append-only strings
|
|
151
|
+
*/
|
|
152
|
+
concat: {
|
|
153
|
+
behavior: 'append',
|
|
154
|
+
merge: (ops: Array<{ value: string; lamportTime: number }>, separator = '\n') => {
|
|
155
|
+
const sorted = ops.sort((a, b) => a.lamportTime - b.lamportTime);
|
|
156
|
+
return sorted.map((op) => op.value).join(separator);
|
|
157
|
+
},
|
|
158
|
+
example: {
|
|
159
|
+
ops: [
|
|
160
|
+
{ value: 'User A edited', lamportTime: 100 },
|
|
161
|
+
{ value: 'User B edited', lamportTime: 101 },
|
|
162
|
+
],
|
|
163
|
+
result: 'User A edited\nUser B edited',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Boolean Data Type
|
|
171
|
+
*
|
|
172
|
+
* dtype: 'boolean'
|
|
173
|
+
* Supported operations: set, or, and
|
|
174
|
+
*/
|
|
175
|
+
export const BooleanType = {
|
|
176
|
+
dtype: 'boolean',
|
|
177
|
+
operations: {
|
|
178
|
+
/**
|
|
179
|
+
* SET - Replace value (LWW)
|
|
180
|
+
* Usage: Flags, toggles
|
|
181
|
+
*/
|
|
182
|
+
set: {
|
|
183
|
+
behavior: 'lww',
|
|
184
|
+
merge: (ops: Array<{ value: boolean; lamportTime: number }>) => {
|
|
185
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
186
|
+
return sorted[0].value;
|
|
187
|
+
},
|
|
188
|
+
example: {
|
|
189
|
+
ops: [
|
|
190
|
+
{ value: true, lamportTime: 100 },
|
|
191
|
+
{ value: false, lamportTime: 101 },
|
|
192
|
+
],
|
|
193
|
+
result: false,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* OR - Logical OR (enable wins)
|
|
199
|
+
* Usage: Feature flags where enabling takes precedence
|
|
200
|
+
*/
|
|
201
|
+
or: {
|
|
202
|
+
behavior: 'or',
|
|
203
|
+
merge: (ops: Array<{ value: boolean; lamportTime: number }>) => {
|
|
204
|
+
return ops.some((op) => op.value === true);
|
|
205
|
+
},
|
|
206
|
+
example: {
|
|
207
|
+
ops: [
|
|
208
|
+
{ value: true, lamportTime: 100 },
|
|
209
|
+
{ value: false, lamportTime: 101 },
|
|
210
|
+
],
|
|
211
|
+
result: true,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* AND - Logical AND (disable wins)
|
|
217
|
+
* Usage: Permissions where disabling takes precedence
|
|
218
|
+
*/
|
|
219
|
+
and: {
|
|
220
|
+
behavior: 'and',
|
|
221
|
+
merge: (ops: Array<{ value: boolean; lamportTime: number }>) => {
|
|
222
|
+
return ops.every((op) => op.value === true);
|
|
223
|
+
},
|
|
224
|
+
example: {
|
|
225
|
+
ops: [
|
|
226
|
+
{ value: true, lamportTime: 100 },
|
|
227
|
+
{ value: false, lamportTime: 101 },
|
|
228
|
+
],
|
|
229
|
+
result: false,
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// ============================================
|
|
236
|
+
// COMPOUND DATA TYPES (3D GRAPHICS)
|
|
237
|
+
// ============================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Vector3 Data Type - [x, y, z]
|
|
241
|
+
*
|
|
242
|
+
* dtype: 'vector3'
|
|
243
|
+
* Supported operations: set, add, multiply, lww-per-component
|
|
244
|
+
* Used for: position, scale, velocity
|
|
245
|
+
*/
|
|
246
|
+
export const Vector3Type = {
|
|
247
|
+
dtype: 'vector3',
|
|
248
|
+
operations: {
|
|
249
|
+
/**
|
|
250
|
+
* SET - Replace entire vector (LWW)
|
|
251
|
+
* Usage: Absolute position/scale
|
|
252
|
+
* Important: Preserves user intent by replacing whole vector
|
|
253
|
+
*/
|
|
254
|
+
set: {
|
|
255
|
+
behavior: 'lww',
|
|
256
|
+
merge: (ops: Array<{ value: [number, number, number]; lamportTime: number }>) => {
|
|
257
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
258
|
+
return sorted[0].value;
|
|
259
|
+
},
|
|
260
|
+
example: {
|
|
261
|
+
ops: [
|
|
262
|
+
{ value: [5, 0, 0], lamportTime: 100 },
|
|
263
|
+
{ value: [0, 5, 0], lamportTime: 101 },
|
|
264
|
+
],
|
|
265
|
+
result: [0, 5, 0],
|
|
266
|
+
// NOT [5, 5, 0] - that would violate user intent!
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* ADD - Component-wise addition
|
|
272
|
+
* Usage: Velocity, forces, deltas
|
|
273
|
+
*/
|
|
274
|
+
add: {
|
|
275
|
+
behavior: 'additive',
|
|
276
|
+
merge: (ops: Array<{ value: [number, number, number]; lamportTime: number }>) => {
|
|
277
|
+
const sum: [number, number, number] = [0, 0, 0];
|
|
278
|
+
for (const op of ops) {
|
|
279
|
+
sum[0] += op.value[0];
|
|
280
|
+
sum[1] += op.value[1];
|
|
281
|
+
sum[2] += op.value[2];
|
|
282
|
+
}
|
|
283
|
+
return sum;
|
|
284
|
+
},
|
|
285
|
+
example: {
|
|
286
|
+
ops: [
|
|
287
|
+
{ value: [1, 0, 0], lamportTime: 100 },
|
|
288
|
+
{ value: [0, 1, 0], lamportTime: 101 },
|
|
289
|
+
],
|
|
290
|
+
result: [1, 1, 0],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* MULTIPLY - Component-wise multiplication
|
|
296
|
+
* Usage: Scale composition
|
|
297
|
+
*/
|
|
298
|
+
multiply: {
|
|
299
|
+
behavior: 'multiplicative',
|
|
300
|
+
merge: (ops: Array<{ value: [number, number, number]; lamportTime: number }>) => {
|
|
301
|
+
const product: [number, number, number] = [1, 1, 1];
|
|
302
|
+
for (const op of ops) {
|
|
303
|
+
product[0] *= op.value[0];
|
|
304
|
+
product[1] *= op.value[1];
|
|
305
|
+
product[2] *= op.value[2];
|
|
306
|
+
}
|
|
307
|
+
return product;
|
|
308
|
+
},
|
|
309
|
+
example: {
|
|
310
|
+
ops: [
|
|
311
|
+
{ value: [2, 1, 1], lamportTime: 100 },
|
|
312
|
+
{ value: [1, 3, 1], lamportTime: 101 },
|
|
313
|
+
],
|
|
314
|
+
result: [2, 3, 1],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* LWW_PER_COMPONENT - LWW per x/y/z
|
|
320
|
+
* Usage: Mixed semantics (rare)
|
|
321
|
+
*/
|
|
322
|
+
lwwPerComponent: {
|
|
323
|
+
behavior: 'lww-per-component',
|
|
324
|
+
merge: (
|
|
325
|
+
ops: Array<{
|
|
326
|
+
value: Partial<{ x: number; y: number; z: number }>;
|
|
327
|
+
lamportTime: number;
|
|
328
|
+
}>
|
|
329
|
+
) => {
|
|
330
|
+
const result = { x: 0, y: 0, z: 0 };
|
|
331
|
+
const latest = { x: -1, y: -1, z: -1 };
|
|
332
|
+
|
|
333
|
+
for (const op of ops) {
|
|
334
|
+
if (op.value.x !== undefined && op.lamportTime > latest.x) {
|
|
335
|
+
result.x = op.value.x;
|
|
336
|
+
latest.x = op.lamportTime;
|
|
337
|
+
}
|
|
338
|
+
if (op.value.y !== undefined && op.lamportTime > latest.y) {
|
|
339
|
+
result.y = op.value.y;
|
|
340
|
+
latest.y = op.lamportTime;
|
|
341
|
+
}
|
|
342
|
+
if (op.value.z !== undefined && op.lamportTime > latest.z) {
|
|
343
|
+
result.z = op.value.z;
|
|
344
|
+
latest.z = op.lamportTime;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return [result.x, result.y, result.z] as [number, number, number];
|
|
349
|
+
},
|
|
350
|
+
example: {
|
|
351
|
+
ops: [
|
|
352
|
+
{ value: { x: 5 }, lamportTime: 100 },
|
|
353
|
+
{ value: { y: 3 }, lamportTime: 101 },
|
|
354
|
+
],
|
|
355
|
+
result: [5, 3, 0],
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Quaternion Data Type - [x, y, z, w]
|
|
363
|
+
*
|
|
364
|
+
* dtype: 'quaternion'
|
|
365
|
+
* Supported operations: set, multiply
|
|
366
|
+
* Used for: rotation
|
|
367
|
+
*/
|
|
368
|
+
export const QuaternionType = {
|
|
369
|
+
dtype: 'quaternion',
|
|
370
|
+
operations: {
|
|
371
|
+
/**
|
|
372
|
+
* SET - Replace entire quaternion (LWW)
|
|
373
|
+
* Usage: Absolute rotation (most common)
|
|
374
|
+
*/
|
|
375
|
+
set: {
|
|
376
|
+
behavior: 'lww',
|
|
377
|
+
merge: (ops: Array<{ value: [number, number, number, number]; lamportTime: number }>) => {
|
|
378
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
379
|
+
return sorted[0].value;
|
|
380
|
+
},
|
|
381
|
+
example: {
|
|
382
|
+
ops: [
|
|
383
|
+
{ value: [0, 0, 0, 1], lamportTime: 100 }, // No rotation
|
|
384
|
+
{ value: [0, 0.707, 0, 0.707], lamportTime: 101 }, // 90° around Y
|
|
385
|
+
],
|
|
386
|
+
result: [0, 0.707, 0, 0.707],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* MULTIPLY - Quaternion multiplication (composition)
|
|
392
|
+
* Usage: Rotation composition (advanced)
|
|
393
|
+
* Warning: Order matters! Result may be unpredictable for users
|
|
394
|
+
*/
|
|
395
|
+
multiply: {
|
|
396
|
+
behavior: 'multiplicative',
|
|
397
|
+
merge: (ops: Array<{ value: [number, number, number, number]; lamportTime: number }>) => {
|
|
398
|
+
// Sort by lamport time to get deterministic order
|
|
399
|
+
const sorted = ops.sort((a, b) => a.lamportTime - b.lamportTime);
|
|
400
|
+
|
|
401
|
+
// Multiply quaternions in order
|
|
402
|
+
let result = sorted[0].value;
|
|
403
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
404
|
+
result = multiplyQuaternions(result, sorted[i].value);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
},
|
|
408
|
+
example: {
|
|
409
|
+
ops: [
|
|
410
|
+
{ value: [0, 0.707, 0, 0.707], lamportTime: 100 }, // 90° Y
|
|
411
|
+
{ value: [0.707, 0, 0, 0.707], lamportTime: 101 }, // 90° X
|
|
412
|
+
],
|
|
413
|
+
result: [0.5, 0.5, 0.5, 0.5], // Combined rotation
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Helper: Quaternion multiplication
|
|
421
|
+
*/
|
|
422
|
+
function multiplyQuaternions(
|
|
423
|
+
a: [number, number, number, number],
|
|
424
|
+
b: [number, number, number, number]
|
|
425
|
+
): [number, number, number, number] {
|
|
426
|
+
const [ax, ay, az, aw] = a;
|
|
427
|
+
const [bx, by, bz, bw] = b;
|
|
428
|
+
|
|
429
|
+
return [
|
|
430
|
+
aw * bx + ax * bw + ay * bz - az * by,
|
|
431
|
+
aw * by - ax * bz + ay * bw + az * bx,
|
|
432
|
+
aw * bz + ax * by - ay * bx + az * bw,
|
|
433
|
+
aw * bw - ax * bx - ay * by - az * bz,
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Color Data Type - RGB hex string or object
|
|
439
|
+
*
|
|
440
|
+
* dtype: 'color'
|
|
441
|
+
* Supported operations: set, blend
|
|
442
|
+
*/
|
|
443
|
+
export const ColorType = {
|
|
444
|
+
dtype: 'color',
|
|
445
|
+
operations: {
|
|
446
|
+
/**
|
|
447
|
+
* SET - Replace color (LWW)
|
|
448
|
+
* Usage: Object colors (most common)
|
|
449
|
+
*/
|
|
450
|
+
set: {
|
|
451
|
+
behavior: 'lww',
|
|
452
|
+
merge: (ops: Array<{ value: string; lamportTime: number }>) => {
|
|
453
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
454
|
+
return sorted[0].value;
|
|
455
|
+
},
|
|
456
|
+
example: {
|
|
457
|
+
ops: [
|
|
458
|
+
{ value: '#ff0000', lamportTime: 100 },
|
|
459
|
+
{ value: '#0000ff', lamportTime: 101 },
|
|
460
|
+
],
|
|
461
|
+
result: '#0000ff',
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* BLEND - Average colors
|
|
467
|
+
* Usage: Lighting (advanced)
|
|
468
|
+
*/
|
|
469
|
+
blend: {
|
|
470
|
+
behavior: 'blend',
|
|
471
|
+
merge: (ops: Array<{ value: string; lamportTime: number }>) => {
|
|
472
|
+
// Convert hex to RGB, average, convert back
|
|
473
|
+
const rgbs = ops.map((op) => hexToRgb(op.value));
|
|
474
|
+
const avgR = Math.round(rgbs.reduce((sum, rgb) => sum + rgb.r, 0) / rgbs.length);
|
|
475
|
+
const avgG = Math.round(rgbs.reduce((sum, rgb) => sum + rgb.g, 0) / rgbs.length);
|
|
476
|
+
const avgB = Math.round(rgbs.reduce((sum, rgb) => sum + rgb.b, 0) / rgbs.length);
|
|
477
|
+
return rgbToHex(avgR, avgG, avgB);
|
|
478
|
+
},
|
|
479
|
+
example: {
|
|
480
|
+
ops: [
|
|
481
|
+
{ value: '#ff0000', lamportTime: 100 }, // Red
|
|
482
|
+
{ value: '#0000ff', lamportTime: 101 }, // Blue
|
|
483
|
+
],
|
|
484
|
+
result: '#7f007f', // Purple
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Helper: Hex to RGB
|
|
492
|
+
*/
|
|
493
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
494
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
495
|
+
return result
|
|
496
|
+
? {
|
|
497
|
+
r: parseInt(result[1], 16),
|
|
498
|
+
g: parseInt(result[2], 16),
|
|
499
|
+
b: parseInt(result[3], 16),
|
|
500
|
+
}
|
|
501
|
+
: { r: 0, g: 0, b: 0 };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Helper: RGB to Hex
|
|
506
|
+
*/
|
|
507
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
508
|
+
return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ============================================
|
|
512
|
+
// COLLECTION DATA TYPES
|
|
513
|
+
// ============================================
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Array Data Type
|
|
517
|
+
*
|
|
518
|
+
* dtype: 'array'
|
|
519
|
+
* Supported operations: set, union, append, rga
|
|
520
|
+
*/
|
|
521
|
+
export const ArrayType = {
|
|
522
|
+
dtype: 'array',
|
|
523
|
+
operations: {
|
|
524
|
+
/**
|
|
525
|
+
* SET - Replace entire array (LWW)
|
|
526
|
+
* Usage: Replace whole list
|
|
527
|
+
*/
|
|
528
|
+
set: {
|
|
529
|
+
behavior: 'lww',
|
|
530
|
+
merge: (ops: Array<{ value: any[]; lamportTime: number }>) => {
|
|
531
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
532
|
+
return sorted[0].value;
|
|
533
|
+
},
|
|
534
|
+
example: {
|
|
535
|
+
ops: [
|
|
536
|
+
{ value: ['a', 'b'], lamportTime: 100 },
|
|
537
|
+
{ value: ['c', 'd'], lamportTime: 101 },
|
|
538
|
+
],
|
|
539
|
+
result: ['c', 'd'],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* UNION - Merge all unique elements
|
|
545
|
+
* Usage: Tags, sets
|
|
546
|
+
*/
|
|
547
|
+
union: {
|
|
548
|
+
behavior: 'union',
|
|
549
|
+
merge: (ops: Array<{ value: any[]; lamportTime: number }>) => {
|
|
550
|
+
const allElements = ops.flatMap((op) => op.value);
|
|
551
|
+
return [...new Set(allElements)];
|
|
552
|
+
},
|
|
553
|
+
example: {
|
|
554
|
+
ops: [
|
|
555
|
+
{ value: ['a', 'b'], lamportTime: 100 },
|
|
556
|
+
{ value: ['b', 'c'], lamportTime: 101 },
|
|
557
|
+
],
|
|
558
|
+
result: ['a', 'b', 'c'],
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* APPEND - Append all (ordered by lamport)
|
|
564
|
+
* Usage: Logs, history
|
|
565
|
+
*/
|
|
566
|
+
append: {
|
|
567
|
+
behavior: 'append',
|
|
568
|
+
merge: (ops: Array<{ value: any[]; lamportTime: number }>) => {
|
|
569
|
+
const sorted = ops.sort((a, b) => a.lamportTime - b.lamportTime);
|
|
570
|
+
return sorted.flatMap((op) => op.value);
|
|
571
|
+
},
|
|
572
|
+
example: {
|
|
573
|
+
ops: [
|
|
574
|
+
{ value: ['edit1'], lamportTime: 100 },
|
|
575
|
+
{ value: ['edit2'], lamportTime: 101 },
|
|
576
|
+
],
|
|
577
|
+
result: ['edit1', 'edit2'],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* RGA - Replicated Growable Array (CRDT)
|
|
583
|
+
* Usage: Text editing, ordered lists (Phase 3)
|
|
584
|
+
*/
|
|
585
|
+
rga: {
|
|
586
|
+
behavior: 'rga',
|
|
587
|
+
merge: (
|
|
588
|
+
ops: Array<{ action: 'insert' | 'delete'; index: number; value?: any; lamportTime: number }>
|
|
589
|
+
) => {
|
|
590
|
+
// Simplified RGA - real implementation needs tombstones and references
|
|
591
|
+
const sorted = ops.sort((a, b) => a.lamportTime - b.lamportTime);
|
|
592
|
+
const result: any[] = [];
|
|
593
|
+
|
|
594
|
+
for (const op of sorted) {
|
|
595
|
+
if (op.action === 'insert') {
|
|
596
|
+
result.splice(op.index, 0, op.value);
|
|
597
|
+
} else if (op.action === 'delete') {
|
|
598
|
+
result.splice(op.index, 1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return result;
|
|
603
|
+
},
|
|
604
|
+
example: {
|
|
605
|
+
ops: [
|
|
606
|
+
{ action: 'insert', index: 0, value: 'a', lamportTime: 100 },
|
|
607
|
+
{ action: 'insert', index: 1, value: 'b', lamportTime: 101 },
|
|
608
|
+
{ action: 'insert', index: 1, value: 'X', lamportTime: 102 },
|
|
609
|
+
],
|
|
610
|
+
result: ['a', 'X', 'b'],
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Object/Map Data Type
|
|
618
|
+
*
|
|
619
|
+
* dtype: 'object'
|
|
620
|
+
* Supported operations: set, lww-per-key
|
|
621
|
+
*/
|
|
622
|
+
export const ObjectType = {
|
|
623
|
+
dtype: 'object',
|
|
624
|
+
operations: {
|
|
625
|
+
/**
|
|
626
|
+
* SET - Replace entire object (LWW)
|
|
627
|
+
* Usage: Replace whole map
|
|
628
|
+
*/
|
|
629
|
+
set: {
|
|
630
|
+
behavior: 'lww',
|
|
631
|
+
merge: (ops: Array<{ value: Record<string, any>; lamportTime: number }>) => {
|
|
632
|
+
const sorted = ops.sort((a, b) => b.lamportTime - a.lamportTime);
|
|
633
|
+
return sorted[0].value;
|
|
634
|
+
},
|
|
635
|
+
example: {
|
|
636
|
+
ops: [
|
|
637
|
+
{ value: { a: 1 }, lamportTime: 100 },
|
|
638
|
+
{ value: { b: 2 }, lamportTime: 101 },
|
|
639
|
+
],
|
|
640
|
+
result: { b: 2 },
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* LWW_PER_KEY - LWW per object key
|
|
646
|
+
* Usage: Properties (most common)
|
|
647
|
+
*/
|
|
648
|
+
lwwPerKey: {
|
|
649
|
+
behavior: 'lww-per-key',
|
|
650
|
+
merge: (
|
|
651
|
+
ops: Array<{ value: Record<string, any>; lamportTime: number; perKeyLamport?: Record<string, number> }>
|
|
652
|
+
) => {
|
|
653
|
+
const result: Record<string, any> = {};
|
|
654
|
+
const latestTimes: Record<string, number> = {};
|
|
655
|
+
|
|
656
|
+
for (const op of ops) {
|
|
657
|
+
for (const [key, value] of Object.entries(op.value)) {
|
|
658
|
+
const keyLamport = op.perKeyLamport?.[key] ?? op.lamportTime;
|
|
659
|
+
if (!latestTimes[key] || keyLamport > latestTimes[key]) {
|
|
660
|
+
result[key] = value;
|
|
661
|
+
latestTimes[key] = keyLamport;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return result;
|
|
667
|
+
},
|
|
668
|
+
example: {
|
|
669
|
+
ops: [
|
|
670
|
+
{ value: { color: 'red', size: 10 }, lamportTime: 100 },
|
|
671
|
+
{ value: { color: 'blue', opacity: 0.5 }, lamportTime: 101 },
|
|
672
|
+
],
|
|
673
|
+
result: { color: 'blue', size: 10, opacity: 0.5 },
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
};
|