@topgunbuild/core 0.1.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/LICENSE +97 -0
- package/dist/index.d.mts +725 -0
- package/dist/index.d.ts +725 -0
- package/dist/index.js +966 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +906 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
// src/HLC.ts
|
|
2
|
+
var _HLC = class _HLC {
|
|
3
|
+
constructor(nodeId) {
|
|
4
|
+
this.nodeId = nodeId;
|
|
5
|
+
this.lastMillis = 0;
|
|
6
|
+
this.lastCounter = 0;
|
|
7
|
+
}
|
|
8
|
+
get getNodeId() {
|
|
9
|
+
return this.nodeId;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generates a new unique timestamp for a local event.
|
|
13
|
+
* Ensures monotonicity: always greater than any previously generated or received timestamp.
|
|
14
|
+
*/
|
|
15
|
+
now() {
|
|
16
|
+
const systemTime = Date.now();
|
|
17
|
+
if (systemTime > this.lastMillis) {
|
|
18
|
+
this.lastMillis = systemTime;
|
|
19
|
+
this.lastCounter = 0;
|
|
20
|
+
} else {
|
|
21
|
+
this.lastCounter++;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
millis: this.lastMillis,
|
|
25
|
+
counter: this.lastCounter,
|
|
26
|
+
nodeId: this.nodeId
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Updates the local clock based on a received remote timestamp.
|
|
31
|
+
* Must be called whenever a message/event is received from another node.
|
|
32
|
+
*/
|
|
33
|
+
update(remote) {
|
|
34
|
+
const systemTime = Date.now();
|
|
35
|
+
if (remote.millis > systemTime + _HLC.MAX_DRIFT) {
|
|
36
|
+
console.warn(`Clock drift detected: Remote time ${remote.millis} is far ahead of local ${systemTime}`);
|
|
37
|
+
}
|
|
38
|
+
const maxMillis = Math.max(this.lastMillis, systemTime, remote.millis);
|
|
39
|
+
if (maxMillis === this.lastMillis && maxMillis === remote.millis) {
|
|
40
|
+
this.lastCounter = Math.max(this.lastCounter, remote.counter) + 1;
|
|
41
|
+
} else if (maxMillis === this.lastMillis) {
|
|
42
|
+
this.lastCounter++;
|
|
43
|
+
} else if (maxMillis === remote.millis) {
|
|
44
|
+
this.lastCounter = remote.counter + 1;
|
|
45
|
+
} else {
|
|
46
|
+
this.lastCounter = 0;
|
|
47
|
+
}
|
|
48
|
+
this.lastMillis = maxMillis;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compares two timestamps.
|
|
52
|
+
* Returns -1 if a < b, 1 if a > b, 0 if equal.
|
|
53
|
+
*/
|
|
54
|
+
static compare(a, b) {
|
|
55
|
+
if (a.millis !== b.millis) {
|
|
56
|
+
return a.millis - b.millis;
|
|
57
|
+
}
|
|
58
|
+
if (a.counter !== b.counter) {
|
|
59
|
+
return a.counter - b.counter;
|
|
60
|
+
}
|
|
61
|
+
return a.nodeId.localeCompare(b.nodeId);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Serializes timestamp to a string representation (e.g., for storage/network).
|
|
65
|
+
* Format: "<millis>:<counter>:<nodeId>"
|
|
66
|
+
*/
|
|
67
|
+
static toString(ts) {
|
|
68
|
+
return `${ts.millis}:${ts.counter}:${ts.nodeId}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parses a string representation back to a Timestamp object.
|
|
72
|
+
*/
|
|
73
|
+
static parse(str) {
|
|
74
|
+
const parts = str.split(":");
|
|
75
|
+
if (parts.length !== 3) {
|
|
76
|
+
throw new Error(`Invalid timestamp format: ${str}`);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
millis: parseInt(parts[0], 10),
|
|
80
|
+
counter: parseInt(parts[1], 10),
|
|
81
|
+
nodeId: parts[2]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
// Max allowable drift in milliseconds (1 minute)
|
|
86
|
+
_HLC.MAX_DRIFT = 6e4;
|
|
87
|
+
var HLC = _HLC;
|
|
88
|
+
|
|
89
|
+
// src/utils/hash.ts
|
|
90
|
+
function hashString(str) {
|
|
91
|
+
let hash = 2166136261;
|
|
92
|
+
for (let i = 0; i < str.length; i++) {
|
|
93
|
+
hash ^= str.charCodeAt(i);
|
|
94
|
+
hash = Math.imul(hash, 16777619);
|
|
95
|
+
}
|
|
96
|
+
return hash >>> 0;
|
|
97
|
+
}
|
|
98
|
+
function combineHashes(hashes) {
|
|
99
|
+
let result = 0;
|
|
100
|
+
for (const h of hashes) {
|
|
101
|
+
result = result + h | 0;
|
|
102
|
+
}
|
|
103
|
+
return result >>> 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/MerkleTree.ts
|
|
107
|
+
var MerkleTree = class {
|
|
108
|
+
constructor(records = /* @__PURE__ */ new Map(), depth = 3) {
|
|
109
|
+
this.depth = depth;
|
|
110
|
+
this.root = { hash: 0, children: {} };
|
|
111
|
+
for (const [key, record] of records) {
|
|
112
|
+
this.update(key, record);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Incrementally updates the Merkle Tree with a single record.
|
|
117
|
+
* @param key The key of the record
|
|
118
|
+
* @param record The record (value + timestamp)
|
|
119
|
+
*/
|
|
120
|
+
update(key, record) {
|
|
121
|
+
const itemHash = hashString(`${key}:${record.timestamp.millis}:${record.timestamp.counter}:${record.timestamp.nodeId}`);
|
|
122
|
+
const pathHash = hashString(key).toString(16).padStart(8, "0");
|
|
123
|
+
this.updateNode(this.root, key, itemHash, pathHash, 0);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Removes a key from the Merkle Tree.
|
|
127
|
+
* Necessary for Garbage Collection of tombstones.
|
|
128
|
+
*/
|
|
129
|
+
remove(key) {
|
|
130
|
+
const pathHash = hashString(key).toString(16).padStart(8, "0");
|
|
131
|
+
this.removeNode(this.root, key, pathHash, 0);
|
|
132
|
+
}
|
|
133
|
+
removeNode(node, key, pathHash, level) {
|
|
134
|
+
if (level >= this.depth) {
|
|
135
|
+
if (node.entries) {
|
|
136
|
+
node.entries.delete(key);
|
|
137
|
+
let h2 = 0;
|
|
138
|
+
for (const val of node.entries.values()) {
|
|
139
|
+
h2 = h2 + val | 0;
|
|
140
|
+
}
|
|
141
|
+
node.hash = h2 >>> 0;
|
|
142
|
+
}
|
|
143
|
+
return node.hash;
|
|
144
|
+
}
|
|
145
|
+
const bucketChar = pathHash[level];
|
|
146
|
+
if (node.children && node.children[bucketChar]) {
|
|
147
|
+
const childHash = this.removeNode(node.children[bucketChar], key, pathHash, level + 1);
|
|
148
|
+
}
|
|
149
|
+
let h = 0;
|
|
150
|
+
if (node.children) {
|
|
151
|
+
for (const child of Object.values(node.children)) {
|
|
152
|
+
h = h + child.hash | 0;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
node.hash = h >>> 0;
|
|
156
|
+
return node.hash;
|
|
157
|
+
}
|
|
158
|
+
updateNode(node, key, itemHash, pathHash, level) {
|
|
159
|
+
if (level >= this.depth) {
|
|
160
|
+
if (!node.entries) node.entries = /* @__PURE__ */ new Map();
|
|
161
|
+
node.entries.set(key, itemHash);
|
|
162
|
+
let h2 = 0;
|
|
163
|
+
for (const val of node.entries.values()) {
|
|
164
|
+
h2 = h2 + val | 0;
|
|
165
|
+
}
|
|
166
|
+
node.hash = h2 >>> 0;
|
|
167
|
+
return node.hash;
|
|
168
|
+
}
|
|
169
|
+
const bucketChar = pathHash[level];
|
|
170
|
+
if (!node.children) node.children = {};
|
|
171
|
+
if (!node.children[bucketChar]) {
|
|
172
|
+
node.children[bucketChar] = { hash: 0 };
|
|
173
|
+
}
|
|
174
|
+
this.updateNode(node.children[bucketChar], key, itemHash, pathHash, level + 1);
|
|
175
|
+
let h = 0;
|
|
176
|
+
for (const child of Object.values(node.children)) {
|
|
177
|
+
h = h + child.hash | 0;
|
|
178
|
+
}
|
|
179
|
+
node.hash = h >>> 0;
|
|
180
|
+
return node.hash;
|
|
181
|
+
}
|
|
182
|
+
getRootHash() {
|
|
183
|
+
return this.root.hash;
|
|
184
|
+
}
|
|
185
|
+
getNode(path) {
|
|
186
|
+
let current = this.root;
|
|
187
|
+
for (const char of path) {
|
|
188
|
+
if (!current.children || !current.children[char]) {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
current = current.children[char];
|
|
192
|
+
}
|
|
193
|
+
return current;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Returns the hashes of the children at the given path.
|
|
197
|
+
* Used by the client/server to compare buckets.
|
|
198
|
+
*/
|
|
199
|
+
getBuckets(path) {
|
|
200
|
+
const node = this.getNode(path);
|
|
201
|
+
if (!node || !node.children) return {};
|
|
202
|
+
const result = {};
|
|
203
|
+
for (const [key, child] of Object.entries(node.children)) {
|
|
204
|
+
result[key] = child.hash;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* For a leaf node (bucket), returns the actual keys it contains.
|
|
210
|
+
* Used to request specific keys when a bucket differs.
|
|
211
|
+
*/
|
|
212
|
+
getKeysInBucket(path) {
|
|
213
|
+
const node = this.getNode(path);
|
|
214
|
+
if (!node || !node.entries) return [];
|
|
215
|
+
return Array.from(node.entries.keys());
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/LWWMap.ts
|
|
220
|
+
var LWWMap = class {
|
|
221
|
+
constructor(hlc) {
|
|
222
|
+
this.listeners = [];
|
|
223
|
+
this.hlc = hlc;
|
|
224
|
+
this.data = /* @__PURE__ */ new Map();
|
|
225
|
+
this.merkleTree = new MerkleTree();
|
|
226
|
+
}
|
|
227
|
+
onChange(callback) {
|
|
228
|
+
this.listeners.push(callback);
|
|
229
|
+
return () => {
|
|
230
|
+
this.listeners = this.listeners.filter((cb) => cb !== callback);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
notify() {
|
|
234
|
+
this.listeners.forEach((cb) => cb());
|
|
235
|
+
}
|
|
236
|
+
getMerkleTree() {
|
|
237
|
+
return this.merkleTree;
|
|
238
|
+
}
|
|
239
|
+
get size() {
|
|
240
|
+
return this.data.size;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Sets a value for a key.
|
|
244
|
+
* Generates a new timestamp using the local HLC.
|
|
245
|
+
*/
|
|
246
|
+
set(key, value, ttlMs) {
|
|
247
|
+
const timestamp = this.hlc.now();
|
|
248
|
+
const record = { value, timestamp };
|
|
249
|
+
if (ttlMs !== void 0) {
|
|
250
|
+
if (typeof ttlMs !== "number" || ttlMs <= 0 || !Number.isFinite(ttlMs)) {
|
|
251
|
+
throw new Error("TTL must be a positive finite number");
|
|
252
|
+
}
|
|
253
|
+
record.ttlMs = ttlMs;
|
|
254
|
+
}
|
|
255
|
+
this.data.set(key, record);
|
|
256
|
+
this.merkleTree.update(String(key), record);
|
|
257
|
+
this.notify();
|
|
258
|
+
return record;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Retrieves the value for a key.
|
|
262
|
+
* Returns undefined if key doesn't exist, is a tombstone, or is expired.
|
|
263
|
+
*/
|
|
264
|
+
get(key) {
|
|
265
|
+
const record = this.data.get(key);
|
|
266
|
+
if (!record || record.value === null) {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
if (record.ttlMs) {
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
if (record.timestamp.millis + record.ttlMs < now) {
|
|
272
|
+
return void 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return record.value;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Returns the full record (including timestamp).
|
|
279
|
+
* Useful for synchronization.
|
|
280
|
+
*/
|
|
281
|
+
getRecord(key) {
|
|
282
|
+
return this.data.get(key);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Removes a key (creates a tombstone).
|
|
286
|
+
*/
|
|
287
|
+
remove(key) {
|
|
288
|
+
const timestamp = this.hlc.now();
|
|
289
|
+
const tombstone = { value: null, timestamp };
|
|
290
|
+
this.data.set(key, tombstone);
|
|
291
|
+
this.merkleTree.update(String(key), tombstone);
|
|
292
|
+
this.notify();
|
|
293
|
+
return tombstone;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Merges a record from a remote source.
|
|
297
|
+
* Returns true if the local state was updated.
|
|
298
|
+
*/
|
|
299
|
+
merge(key, remoteRecord) {
|
|
300
|
+
this.hlc.update(remoteRecord.timestamp);
|
|
301
|
+
const localRecord = this.data.get(key);
|
|
302
|
+
if (!localRecord || HLC.compare(remoteRecord.timestamp, localRecord.timestamp) > 0) {
|
|
303
|
+
this.data.set(key, remoteRecord);
|
|
304
|
+
this.merkleTree.update(String(key), remoteRecord);
|
|
305
|
+
this.notify();
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Garbage Collection: Prunes tombstones older than the specified timestamp.
|
|
312
|
+
* Only removes records that are tombstones (deleted) AND older than the threshold.
|
|
313
|
+
*
|
|
314
|
+
* @param olderThan The timestamp threshold. Tombstones older than this will be removed.
|
|
315
|
+
* @returns The number of tombstones removed.
|
|
316
|
+
*/
|
|
317
|
+
prune(olderThan) {
|
|
318
|
+
const removedKeys = [];
|
|
319
|
+
for (const [key, record] of this.data.entries()) {
|
|
320
|
+
if (record.value === null) {
|
|
321
|
+
if (HLC.compare(record.timestamp, olderThan) < 0) {
|
|
322
|
+
this.data.delete(key);
|
|
323
|
+
this.merkleTree.remove(String(key));
|
|
324
|
+
removedKeys.push(key);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (removedKeys.length > 0) {
|
|
329
|
+
this.notify();
|
|
330
|
+
}
|
|
331
|
+
return removedKeys;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Clears all data and tombstones.
|
|
335
|
+
* Resets the MerkleTree.
|
|
336
|
+
*/
|
|
337
|
+
clear() {
|
|
338
|
+
this.data.clear();
|
|
339
|
+
this.merkleTree = new MerkleTree();
|
|
340
|
+
this.notify();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Returns an iterator over all non-deleted entries.
|
|
344
|
+
*/
|
|
345
|
+
entries() {
|
|
346
|
+
const iterator = this.data.entries();
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
return {
|
|
349
|
+
[Symbol.iterator]() {
|
|
350
|
+
return this;
|
|
351
|
+
},
|
|
352
|
+
next: () => {
|
|
353
|
+
let result = iterator.next();
|
|
354
|
+
while (!result.done) {
|
|
355
|
+
const [key, record] = result.value;
|
|
356
|
+
if (record.value !== null) {
|
|
357
|
+
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
358
|
+
result = iterator.next();
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
return { value: [key, record.value], done: false };
|
|
362
|
+
}
|
|
363
|
+
result = iterator.next();
|
|
364
|
+
}
|
|
365
|
+
return { value: void 0, done: true };
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Returns all keys (including tombstones).
|
|
371
|
+
*/
|
|
372
|
+
allKeys() {
|
|
373
|
+
return this.data.keys();
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// src/ORMap.ts
|
|
378
|
+
var ORMap = class {
|
|
379
|
+
constructor(hlc) {
|
|
380
|
+
this.listeners = [];
|
|
381
|
+
this.hlc = hlc;
|
|
382
|
+
this.items = /* @__PURE__ */ new Map();
|
|
383
|
+
this.tombstones = /* @__PURE__ */ new Set();
|
|
384
|
+
}
|
|
385
|
+
onChange(callback) {
|
|
386
|
+
this.listeners.push(callback);
|
|
387
|
+
return () => {
|
|
388
|
+
this.listeners = this.listeners.filter((cb) => cb !== callback);
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
notify() {
|
|
392
|
+
this.listeners.forEach((cb) => cb());
|
|
393
|
+
}
|
|
394
|
+
get size() {
|
|
395
|
+
return this.items.size;
|
|
396
|
+
}
|
|
397
|
+
get totalRecords() {
|
|
398
|
+
let count = 0;
|
|
399
|
+
for (const keyMap of this.items.values()) {
|
|
400
|
+
count += keyMap.size;
|
|
401
|
+
}
|
|
402
|
+
return count;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Adds a value to the set associated with the key.
|
|
406
|
+
* Generates a unique tag for this specific addition.
|
|
407
|
+
*/
|
|
408
|
+
add(key, value, ttlMs) {
|
|
409
|
+
const timestamp = this.hlc.now();
|
|
410
|
+
const tag = HLC.toString(timestamp);
|
|
411
|
+
const record = {
|
|
412
|
+
value,
|
|
413
|
+
timestamp,
|
|
414
|
+
tag
|
|
415
|
+
};
|
|
416
|
+
if (ttlMs !== void 0) {
|
|
417
|
+
if (typeof ttlMs !== "number" || ttlMs <= 0 || !Number.isFinite(ttlMs)) {
|
|
418
|
+
throw new Error("TTL must be a positive finite number");
|
|
419
|
+
}
|
|
420
|
+
record.ttlMs = ttlMs;
|
|
421
|
+
}
|
|
422
|
+
let keyMap = this.items.get(key);
|
|
423
|
+
if (!keyMap) {
|
|
424
|
+
keyMap = /* @__PURE__ */ new Map();
|
|
425
|
+
this.items.set(key, keyMap);
|
|
426
|
+
}
|
|
427
|
+
keyMap.set(tag, record);
|
|
428
|
+
this.notify();
|
|
429
|
+
return record;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Removes a specific value from the set associated with the key.
|
|
433
|
+
* Marks all *currently observed* instances of this value as removed (tombstones).
|
|
434
|
+
* Returns the list of tags that were removed (useful for sync).
|
|
435
|
+
*/
|
|
436
|
+
remove(key, value) {
|
|
437
|
+
const keyMap = this.items.get(key);
|
|
438
|
+
if (!keyMap) return [];
|
|
439
|
+
const tagsToRemove = [];
|
|
440
|
+
for (const [tag, record] of keyMap.entries()) {
|
|
441
|
+
if (record.value === value) {
|
|
442
|
+
tagsToRemove.push(tag);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const tag of tagsToRemove) {
|
|
446
|
+
this.tombstones.add(tag);
|
|
447
|
+
keyMap.delete(tag);
|
|
448
|
+
}
|
|
449
|
+
if (keyMap.size === 0) {
|
|
450
|
+
this.items.delete(key);
|
|
451
|
+
}
|
|
452
|
+
this.notify();
|
|
453
|
+
return tagsToRemove;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Clears all data and tombstones.
|
|
457
|
+
*/
|
|
458
|
+
clear() {
|
|
459
|
+
this.items.clear();
|
|
460
|
+
this.tombstones.clear();
|
|
461
|
+
this.notify();
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Returns all active values for a key.
|
|
465
|
+
* Filters out expired records.
|
|
466
|
+
*/
|
|
467
|
+
get(key) {
|
|
468
|
+
const keyMap = this.items.get(key);
|
|
469
|
+
if (!keyMap) return [];
|
|
470
|
+
const values = [];
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
for (const [tag, record] of keyMap.entries()) {
|
|
473
|
+
if (!this.tombstones.has(tag)) {
|
|
474
|
+
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
values.push(record.value);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return values;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Returns all active records for a key.
|
|
484
|
+
* Useful for persistence and sync.
|
|
485
|
+
* Filters out expired records.
|
|
486
|
+
*/
|
|
487
|
+
getRecords(key) {
|
|
488
|
+
const keyMap = this.items.get(key);
|
|
489
|
+
if (!keyMap) return [];
|
|
490
|
+
const records = [];
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
for (const [tag, record] of keyMap.entries()) {
|
|
493
|
+
if (!this.tombstones.has(tag)) {
|
|
494
|
+
if (record.ttlMs && record.timestamp.millis + record.ttlMs < now) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
records.push(record);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return records;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Returns all tombstone tags.
|
|
504
|
+
*/
|
|
505
|
+
getTombstones() {
|
|
506
|
+
return Array.from(this.tombstones);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Applies a record from a remote source (Sync).
|
|
510
|
+
*/
|
|
511
|
+
apply(key, record) {
|
|
512
|
+
if (this.tombstones.has(record.tag)) return;
|
|
513
|
+
let keyMap = this.items.get(key);
|
|
514
|
+
if (!keyMap) {
|
|
515
|
+
keyMap = /* @__PURE__ */ new Map();
|
|
516
|
+
this.items.set(key, keyMap);
|
|
517
|
+
}
|
|
518
|
+
keyMap.set(record.tag, record);
|
|
519
|
+
this.hlc.update(record.timestamp);
|
|
520
|
+
this.notify();
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Applies a tombstone (deletion) from a remote source.
|
|
524
|
+
*/
|
|
525
|
+
applyTombstone(tag) {
|
|
526
|
+
this.tombstones.add(tag);
|
|
527
|
+
for (const [key, keyMap] of this.items) {
|
|
528
|
+
if (keyMap.has(tag)) {
|
|
529
|
+
keyMap.delete(tag);
|
|
530
|
+
if (keyMap.size === 0) this.items.delete(key);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
this.notify();
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Merges state from another ORMap.
|
|
538
|
+
* - Adds all new tombstones from 'other'.
|
|
539
|
+
* - Adds all new items from 'other' that are not in tombstones.
|
|
540
|
+
* - Updates HLC with observed timestamps.
|
|
541
|
+
*/
|
|
542
|
+
merge(other) {
|
|
543
|
+
for (const tag of other.tombstones) {
|
|
544
|
+
this.tombstones.add(tag);
|
|
545
|
+
}
|
|
546
|
+
for (const [key, otherKeyMap] of other.items) {
|
|
547
|
+
let localKeyMap = this.items.get(key);
|
|
548
|
+
if (!localKeyMap) {
|
|
549
|
+
localKeyMap = /* @__PURE__ */ new Map();
|
|
550
|
+
this.items.set(key, localKeyMap);
|
|
551
|
+
}
|
|
552
|
+
for (const [tag, record] of otherKeyMap) {
|
|
553
|
+
if (!this.tombstones.has(tag)) {
|
|
554
|
+
if (!localKeyMap.has(tag)) {
|
|
555
|
+
localKeyMap.set(tag, record);
|
|
556
|
+
}
|
|
557
|
+
this.hlc.update(record.timestamp);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
for (const [key, localKeyMap] of this.items) {
|
|
562
|
+
for (const tag of localKeyMap.keys()) {
|
|
563
|
+
if (this.tombstones.has(tag)) {
|
|
564
|
+
localKeyMap.delete(tag);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (localKeyMap.size === 0) {
|
|
568
|
+
this.items.delete(key);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
this.notify();
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Garbage Collection: Prunes tombstones older than the specified timestamp.
|
|
575
|
+
*/
|
|
576
|
+
prune(olderThan) {
|
|
577
|
+
const removedTags = [];
|
|
578
|
+
for (const tag of this.tombstones) {
|
|
579
|
+
try {
|
|
580
|
+
const timestamp = HLC.parse(tag);
|
|
581
|
+
if (HLC.compare(timestamp, olderThan) < 0) {
|
|
582
|
+
this.tombstones.delete(tag);
|
|
583
|
+
removedTags.push(tag);
|
|
584
|
+
}
|
|
585
|
+
} catch (e) {
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return removedTags;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/serializer.ts
|
|
593
|
+
import { encode, decode } from "@msgpack/msgpack";
|
|
594
|
+
function serialize(data) {
|
|
595
|
+
return encode(data);
|
|
596
|
+
}
|
|
597
|
+
function deserialize(data) {
|
|
598
|
+
return decode(data);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/predicate.ts
|
|
602
|
+
var Predicates = class {
|
|
603
|
+
static equal(attribute, value) {
|
|
604
|
+
return { op: "eq", attribute, value };
|
|
605
|
+
}
|
|
606
|
+
static notEqual(attribute, value) {
|
|
607
|
+
return { op: "neq", attribute, value };
|
|
608
|
+
}
|
|
609
|
+
static greaterThan(attribute, value) {
|
|
610
|
+
return { op: "gt", attribute, value };
|
|
611
|
+
}
|
|
612
|
+
static greaterThanOrEqual(attribute, value) {
|
|
613
|
+
return { op: "gte", attribute, value };
|
|
614
|
+
}
|
|
615
|
+
static lessThan(attribute, value) {
|
|
616
|
+
return { op: "lt", attribute, value };
|
|
617
|
+
}
|
|
618
|
+
static lessThanOrEqual(attribute, value) {
|
|
619
|
+
return { op: "lte", attribute, value };
|
|
620
|
+
}
|
|
621
|
+
static like(attribute, pattern) {
|
|
622
|
+
return { op: "like", attribute, value: pattern };
|
|
623
|
+
}
|
|
624
|
+
static regex(attribute, pattern) {
|
|
625
|
+
return { op: "regex", attribute, value: pattern };
|
|
626
|
+
}
|
|
627
|
+
static between(attribute, from, to) {
|
|
628
|
+
return {
|
|
629
|
+
op: "and",
|
|
630
|
+
children: [
|
|
631
|
+
{ op: "gte", attribute, value: from },
|
|
632
|
+
{ op: "lte", attribute, value: to }
|
|
633
|
+
]
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
static and(...predicates) {
|
|
637
|
+
return { op: "and", children: predicates };
|
|
638
|
+
}
|
|
639
|
+
static or(...predicates) {
|
|
640
|
+
return { op: "or", children: predicates };
|
|
641
|
+
}
|
|
642
|
+
static not(predicate) {
|
|
643
|
+
return { op: "not", children: [predicate] };
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function evaluatePredicate(predicate, data) {
|
|
647
|
+
if (!data) return false;
|
|
648
|
+
switch (predicate.op) {
|
|
649
|
+
case "and":
|
|
650
|
+
return (predicate.children || []).every((p) => evaluatePredicate(p, data));
|
|
651
|
+
case "or":
|
|
652
|
+
return (predicate.children || []).some((p) => evaluatePredicate(p, data));
|
|
653
|
+
case "not": {
|
|
654
|
+
const child = (predicate.children || [])[0];
|
|
655
|
+
if (!child) return true;
|
|
656
|
+
return !evaluatePredicate(child, data);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (!predicate.attribute) return false;
|
|
660
|
+
const value = data[predicate.attribute];
|
|
661
|
+
const target = predicate.value;
|
|
662
|
+
switch (predicate.op) {
|
|
663
|
+
case "eq":
|
|
664
|
+
return value === target;
|
|
665
|
+
case "neq":
|
|
666
|
+
return value !== target;
|
|
667
|
+
case "gt":
|
|
668
|
+
return value > target;
|
|
669
|
+
case "gte":
|
|
670
|
+
return value >= target;
|
|
671
|
+
case "lt":
|
|
672
|
+
return value < target;
|
|
673
|
+
case "lte":
|
|
674
|
+
return value <= target;
|
|
675
|
+
case "like":
|
|
676
|
+
if (typeof value !== "string" || typeof target !== "string") return false;
|
|
677
|
+
const pattern = target.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
|
|
678
|
+
return new RegExp(`^${pattern}$`, "i").test(value);
|
|
679
|
+
case "regex":
|
|
680
|
+
if (typeof value !== "string" || typeof target !== "string") return false;
|
|
681
|
+
return new RegExp(target).test(value);
|
|
682
|
+
default:
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/schemas.ts
|
|
688
|
+
import { z } from "zod";
|
|
689
|
+
var TimestampSchema = z.object({
|
|
690
|
+
millis: z.number(),
|
|
691
|
+
counter: z.number(),
|
|
692
|
+
nodeId: z.string()
|
|
693
|
+
});
|
|
694
|
+
var LWWRecordSchema = z.object({
|
|
695
|
+
value: z.any().nullable(),
|
|
696
|
+
timestamp: TimestampSchema,
|
|
697
|
+
ttlMs: z.number().optional()
|
|
698
|
+
});
|
|
699
|
+
var ORMapRecordSchema = z.object({
|
|
700
|
+
value: z.any(),
|
|
701
|
+
timestamp: TimestampSchema,
|
|
702
|
+
tag: z.string(),
|
|
703
|
+
ttlMs: z.number().optional()
|
|
704
|
+
});
|
|
705
|
+
var PredicateOpSchema = z.enum([
|
|
706
|
+
"eq",
|
|
707
|
+
"neq",
|
|
708
|
+
"gt",
|
|
709
|
+
"gte",
|
|
710
|
+
"lt",
|
|
711
|
+
"lte",
|
|
712
|
+
"like",
|
|
713
|
+
"regex",
|
|
714
|
+
"and",
|
|
715
|
+
"or",
|
|
716
|
+
"not"
|
|
717
|
+
]);
|
|
718
|
+
var PredicateNodeSchema = z.lazy(() => z.object({
|
|
719
|
+
op: PredicateOpSchema,
|
|
720
|
+
attribute: z.string().optional(),
|
|
721
|
+
value: z.any().optional(),
|
|
722
|
+
children: z.array(PredicateNodeSchema).optional()
|
|
723
|
+
}));
|
|
724
|
+
var QuerySchema = z.object({
|
|
725
|
+
where: z.record(z.string(), z.any()).optional(),
|
|
726
|
+
predicate: PredicateNodeSchema.optional(),
|
|
727
|
+
sort: z.record(z.string(), z.enum(["asc", "desc"])).optional(),
|
|
728
|
+
limit: z.number().optional(),
|
|
729
|
+
offset: z.number().optional()
|
|
730
|
+
});
|
|
731
|
+
var ClientOpSchema = z.object({
|
|
732
|
+
id: z.string().optional(),
|
|
733
|
+
mapName: z.string(),
|
|
734
|
+
key: z.string(),
|
|
735
|
+
// Permissive opType to match ServerCoordinator behavior logic
|
|
736
|
+
// It can be 'REMOVE', 'OR_ADD', 'OR_REMOVE' or undefined/other (implies PUT/LWW)
|
|
737
|
+
opType: z.string().optional(),
|
|
738
|
+
record: LWWRecordSchema.nullable().optional(),
|
|
739
|
+
orRecord: ORMapRecordSchema.nullable().optional(),
|
|
740
|
+
orTag: z.string().nullable().optional()
|
|
741
|
+
});
|
|
742
|
+
var AuthMessageSchema = z.object({
|
|
743
|
+
type: z.literal("AUTH"),
|
|
744
|
+
token: z.string()
|
|
745
|
+
});
|
|
746
|
+
var QuerySubMessageSchema = z.object({
|
|
747
|
+
type: z.literal("QUERY_SUB"),
|
|
748
|
+
payload: z.object({
|
|
749
|
+
queryId: z.string(),
|
|
750
|
+
mapName: z.string(),
|
|
751
|
+
query: QuerySchema
|
|
752
|
+
})
|
|
753
|
+
});
|
|
754
|
+
var QueryUnsubMessageSchema = z.object({
|
|
755
|
+
type: z.literal("QUERY_UNSUB"),
|
|
756
|
+
payload: z.object({
|
|
757
|
+
queryId: z.string()
|
|
758
|
+
})
|
|
759
|
+
});
|
|
760
|
+
var ClientOpMessageSchema = z.object({
|
|
761
|
+
type: z.literal("CLIENT_OP"),
|
|
762
|
+
payload: ClientOpSchema
|
|
763
|
+
});
|
|
764
|
+
var OpBatchMessageSchema = z.object({
|
|
765
|
+
type: z.literal("OP_BATCH"),
|
|
766
|
+
payload: z.object({
|
|
767
|
+
ops: z.array(ClientOpSchema)
|
|
768
|
+
})
|
|
769
|
+
});
|
|
770
|
+
var SyncInitMessageSchema = z.object({
|
|
771
|
+
type: z.literal("SYNC_INIT"),
|
|
772
|
+
mapName: z.string(),
|
|
773
|
+
lastSyncTimestamp: z.number().optional()
|
|
774
|
+
});
|
|
775
|
+
var SyncRespRootMessageSchema = z.object({
|
|
776
|
+
type: z.literal("SYNC_RESP_ROOT"),
|
|
777
|
+
payload: z.object({
|
|
778
|
+
mapName: z.string(),
|
|
779
|
+
rootHash: z.number(),
|
|
780
|
+
timestamp: TimestampSchema
|
|
781
|
+
})
|
|
782
|
+
});
|
|
783
|
+
var SyncRespBucketsMessageSchema = z.object({
|
|
784
|
+
type: z.literal("SYNC_RESP_BUCKETS"),
|
|
785
|
+
payload: z.object({
|
|
786
|
+
mapName: z.string(),
|
|
787
|
+
path: z.string(),
|
|
788
|
+
buckets: z.record(z.string(), z.number())
|
|
789
|
+
})
|
|
790
|
+
});
|
|
791
|
+
var SyncRespLeafMessageSchema = z.object({
|
|
792
|
+
type: z.literal("SYNC_RESP_LEAF"),
|
|
793
|
+
payload: z.object({
|
|
794
|
+
mapName: z.string(),
|
|
795
|
+
path: z.string(),
|
|
796
|
+
records: z.array(z.object({
|
|
797
|
+
key: z.string(),
|
|
798
|
+
record: LWWRecordSchema
|
|
799
|
+
}))
|
|
800
|
+
})
|
|
801
|
+
});
|
|
802
|
+
var MerkleReqBucketMessageSchema = z.object({
|
|
803
|
+
type: z.literal("MERKLE_REQ_BUCKET"),
|
|
804
|
+
payload: z.object({
|
|
805
|
+
mapName: z.string(),
|
|
806
|
+
path: z.string()
|
|
807
|
+
})
|
|
808
|
+
});
|
|
809
|
+
var LockRequestSchema = z.object({
|
|
810
|
+
type: z.literal("LOCK_REQUEST"),
|
|
811
|
+
payload: z.object({
|
|
812
|
+
requestId: z.string(),
|
|
813
|
+
name: z.string(),
|
|
814
|
+
ttl: z.number().optional()
|
|
815
|
+
})
|
|
816
|
+
});
|
|
817
|
+
var LockReleaseSchema = z.object({
|
|
818
|
+
type: z.literal("LOCK_RELEASE"),
|
|
819
|
+
payload: z.object({
|
|
820
|
+
requestId: z.string().optional(),
|
|
821
|
+
name: z.string(),
|
|
822
|
+
fencingToken: z.number()
|
|
823
|
+
})
|
|
824
|
+
});
|
|
825
|
+
var TopicSubSchema = z.object({
|
|
826
|
+
type: z.literal("TOPIC_SUB"),
|
|
827
|
+
payload: z.object({
|
|
828
|
+
topic: z.string()
|
|
829
|
+
})
|
|
830
|
+
});
|
|
831
|
+
var TopicUnsubSchema = z.object({
|
|
832
|
+
type: z.literal("TOPIC_UNSUB"),
|
|
833
|
+
payload: z.object({
|
|
834
|
+
topic: z.string()
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
var TopicPubSchema = z.object({
|
|
838
|
+
type: z.literal("TOPIC_PUB"),
|
|
839
|
+
payload: z.object({
|
|
840
|
+
topic: z.string(),
|
|
841
|
+
data: z.any()
|
|
842
|
+
})
|
|
843
|
+
});
|
|
844
|
+
var TopicMessageEventSchema = z.object({
|
|
845
|
+
type: z.literal("TOPIC_MESSAGE"),
|
|
846
|
+
payload: z.object({
|
|
847
|
+
topic: z.string(),
|
|
848
|
+
data: z.any(),
|
|
849
|
+
publisherId: z.string().optional(),
|
|
850
|
+
timestamp: z.number()
|
|
851
|
+
})
|
|
852
|
+
});
|
|
853
|
+
var MessageSchema = z.discriminatedUnion("type", [
|
|
854
|
+
AuthMessageSchema,
|
|
855
|
+
QuerySubMessageSchema,
|
|
856
|
+
QueryUnsubMessageSchema,
|
|
857
|
+
ClientOpMessageSchema,
|
|
858
|
+
OpBatchMessageSchema,
|
|
859
|
+
SyncInitMessageSchema,
|
|
860
|
+
SyncRespRootMessageSchema,
|
|
861
|
+
SyncRespBucketsMessageSchema,
|
|
862
|
+
SyncRespLeafMessageSchema,
|
|
863
|
+
MerkleReqBucketMessageSchema,
|
|
864
|
+
LockRequestSchema,
|
|
865
|
+
LockReleaseSchema,
|
|
866
|
+
TopicSubSchema,
|
|
867
|
+
TopicUnsubSchema,
|
|
868
|
+
TopicPubSchema
|
|
869
|
+
]);
|
|
870
|
+
export {
|
|
871
|
+
AuthMessageSchema,
|
|
872
|
+
ClientOpMessageSchema,
|
|
873
|
+
ClientOpSchema,
|
|
874
|
+
HLC,
|
|
875
|
+
LWWMap,
|
|
876
|
+
LWWRecordSchema,
|
|
877
|
+
LockReleaseSchema,
|
|
878
|
+
LockRequestSchema,
|
|
879
|
+
MerkleReqBucketMessageSchema,
|
|
880
|
+
MerkleTree,
|
|
881
|
+
MessageSchema,
|
|
882
|
+
ORMap,
|
|
883
|
+
ORMapRecordSchema,
|
|
884
|
+
OpBatchMessageSchema,
|
|
885
|
+
PredicateNodeSchema,
|
|
886
|
+
PredicateOpSchema,
|
|
887
|
+
Predicates,
|
|
888
|
+
QuerySchema,
|
|
889
|
+
QuerySubMessageSchema,
|
|
890
|
+
QueryUnsubMessageSchema,
|
|
891
|
+
SyncInitMessageSchema,
|
|
892
|
+
SyncRespBucketsMessageSchema,
|
|
893
|
+
SyncRespLeafMessageSchema,
|
|
894
|
+
SyncRespRootMessageSchema,
|
|
895
|
+
TimestampSchema,
|
|
896
|
+
TopicMessageEventSchema,
|
|
897
|
+
TopicPubSchema,
|
|
898
|
+
TopicSubSchema,
|
|
899
|
+
TopicUnsubSchema,
|
|
900
|
+
combineHashes,
|
|
901
|
+
deserialize,
|
|
902
|
+
evaluatePredicate,
|
|
903
|
+
hashString,
|
|
904
|
+
serialize
|
|
905
|
+
};
|
|
906
|
+
//# sourceMappingURL=index.mjs.map
|