@topgunbuild/server 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/dist/index.js ADDED
@@ -0,0 +1,2915 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ MemoryServerAdapter: () => MemoryServerAdapter,
34
+ PostgresAdapter: () => PostgresAdapter,
35
+ RateLimitInterceptor: () => RateLimitInterceptor,
36
+ SecurityManager: () => SecurityManager,
37
+ ServerCoordinator: () => ServerCoordinator,
38
+ TimestampInterceptor: () => TimestampInterceptor,
39
+ logger: () => logger
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/ServerCoordinator.ts
44
+ var import_http = require("http");
45
+ var import_https = require("https");
46
+ var import_fs2 = require("fs");
47
+ var import_ws2 = require("ws");
48
+ var import_core4 = require("@topgunbuild/core");
49
+ var jwt = __toESM(require("jsonwebtoken"));
50
+ var crypto = __toESM(require("crypto"));
51
+
52
+ // src/query/Matcher.ts
53
+ var import_core = require("@topgunbuild/core");
54
+ function matchesQuery(record, query) {
55
+ const data = record.value;
56
+ if (!data) return false;
57
+ if (record.ttlMs) {
58
+ const now = Date.now();
59
+ if (record.timestamp.millis + record.ttlMs < now) {
60
+ return false;
61
+ }
62
+ }
63
+ if (query.predicate) {
64
+ return (0, import_core.evaluatePredicate)(query.predicate, data);
65
+ }
66
+ if (!query.where) return true;
67
+ for (const [field, expected] of Object.entries(query.where)) {
68
+ const actual = data[field];
69
+ if (typeof expected === "object" && expected !== null && !Array.isArray(expected)) {
70
+ for (const [op, opValueRaw] of Object.entries(expected)) {
71
+ const opValue = opValueRaw;
72
+ switch (op) {
73
+ case "$gt":
74
+ if (!(actual > opValue)) return false;
75
+ break;
76
+ case "$gte":
77
+ if (!(actual >= opValue)) return false;
78
+ break;
79
+ case "$lt":
80
+ if (!(actual < opValue)) return false;
81
+ break;
82
+ case "$lte":
83
+ if (!(actual <= opValue)) return false;
84
+ break;
85
+ case "$ne":
86
+ if (!(actual !== opValue)) return false;
87
+ break;
88
+ // Add more operators as needed ($in, etc.)
89
+ default:
90
+ return false;
91
+ }
92
+ }
93
+ } else {
94
+ if (actual !== expected) {
95
+ return false;
96
+ }
97
+ }
98
+ }
99
+ return true;
100
+ }
101
+ function executeQuery(records, query) {
102
+ if (!query) {
103
+ query = {};
104
+ }
105
+ let results = [];
106
+ if (records instanceof Map) {
107
+ for (const [key, record] of records) {
108
+ if (matchesQuery(record, query)) {
109
+ results.push({ key, record });
110
+ }
111
+ }
112
+ } else {
113
+ for (const record of records) {
114
+ if (matchesQuery(record, query)) {
115
+ results.push({ key: "?", record });
116
+ }
117
+ }
118
+ }
119
+ if (query.sort) {
120
+ results.sort((a, b) => {
121
+ for (const [field, direction] of Object.entries(query.sort)) {
122
+ const valA = a.record.value[field];
123
+ const valB = b.record.value[field];
124
+ if (valA < valB) return direction === "asc" ? -1 : 1;
125
+ if (valA > valB) return direction === "asc" ? 1 : -1;
126
+ }
127
+ return 0;
128
+ });
129
+ }
130
+ if (query.offset || query.limit) {
131
+ const offset = query.offset || 0;
132
+ const limit = query.limit || results.length;
133
+ results = results.slice(offset, offset + limit);
134
+ }
135
+ return results.map((r) => ({ key: r.key, value: r.record.value }));
136
+ }
137
+
138
+ // src/query/QueryRegistry.ts
139
+ var import_core2 = require("@topgunbuild/core");
140
+
141
+ // src/utils/logger.ts
142
+ var import_pino = __toESM(require("pino"));
143
+ var logLevel = process.env.LOG_LEVEL || "info";
144
+ var logger = (0, import_pino.default)({
145
+ level: logLevel,
146
+ transport: process.env.NODE_ENV !== "production" ? {
147
+ target: "pino-pretty",
148
+ options: {
149
+ colorize: true,
150
+ translateTime: "SYS:standard",
151
+ ignore: "pid,hostname"
152
+ }
153
+ } : void 0,
154
+ formatters: {
155
+ level: (label) => {
156
+ return { level: label };
157
+ }
158
+ }
159
+ });
160
+
161
+ // src/query/QueryRegistry.ts
162
+ var ReverseQueryIndex = class {
163
+ constructor() {
164
+ // field -> value -> Set<Subscription>
165
+ this.equality = /* @__PURE__ */ new Map();
166
+ // field -> Set<Subscription>
167
+ this.interest = /* @__PURE__ */ new Map();
168
+ // catch-all
169
+ this.wildcard = /* @__PURE__ */ new Set();
170
+ }
171
+ add(sub) {
172
+ const query = sub.query;
173
+ let indexed = false;
174
+ const cleanupFns = [];
175
+ if (query.where) {
176
+ for (const [field, value] of Object.entries(query.where)) {
177
+ if (typeof value !== "object") {
178
+ this.addEquality(field, value, sub);
179
+ cleanupFns.push(() => this.removeEquality(field, value, sub));
180
+ indexed = true;
181
+ } else {
182
+ this.addInterest(field, sub);
183
+ cleanupFns.push(() => this.removeInterest(field, sub));
184
+ indexed = true;
185
+ }
186
+ }
187
+ }
188
+ if (query.predicate) {
189
+ const visit = (node) => {
190
+ if (node.op === "eq" && node.attribute && node.value !== void 0) {
191
+ this.addEquality(node.attribute, node.value, sub);
192
+ cleanupFns.push(() => this.removeEquality(node.attribute, node.value, sub));
193
+ indexed = true;
194
+ } else if (node.attribute) {
195
+ this.addInterest(node.attribute, sub);
196
+ cleanupFns.push(() => this.removeInterest(node.attribute, sub));
197
+ indexed = true;
198
+ }
199
+ if (node.children) {
200
+ node.children.forEach(visit);
201
+ }
202
+ };
203
+ visit(query.predicate);
204
+ }
205
+ if (query.sort) {
206
+ Object.keys(query.sort).forEach((k) => {
207
+ this.addInterest(k, sub);
208
+ cleanupFns.push(() => this.removeInterest(k, sub));
209
+ indexed = true;
210
+ });
211
+ }
212
+ if (!indexed) {
213
+ this.wildcard.add(sub);
214
+ cleanupFns.push(() => this.wildcard.delete(sub));
215
+ }
216
+ sub._cleanup = () => cleanupFns.forEach((fn) => fn());
217
+ }
218
+ remove(sub) {
219
+ if (sub._cleanup) {
220
+ sub._cleanup();
221
+ sub._cleanup = void 0;
222
+ }
223
+ }
224
+ getCandidates(changedFields, oldVal, newVal) {
225
+ const candidates = new Set(this.wildcard);
226
+ if (changedFields === "ALL") {
227
+ for (const set of this.interest.values()) {
228
+ for (const s of set) candidates.add(s);
229
+ }
230
+ for (const map of this.equality.values()) {
231
+ for (const set of map.values()) {
232
+ for (const s of set) candidates.add(s);
233
+ }
234
+ }
235
+ return candidates;
236
+ }
237
+ if (changedFields.size === 0) return candidates;
238
+ for (const field of changedFields) {
239
+ if (this.interest.has(field)) {
240
+ for (const sub of this.interest.get(field)) {
241
+ candidates.add(sub);
242
+ }
243
+ }
244
+ if (this.equality.has(field)) {
245
+ const valMap = this.equality.get(field);
246
+ if (newVal && newVal[field] !== void 0 && valMap.has(newVal[field])) {
247
+ for (const sub of valMap.get(newVal[field])) {
248
+ candidates.add(sub);
249
+ }
250
+ }
251
+ if (oldVal && oldVal[field] !== void 0 && valMap.has(oldVal[field])) {
252
+ for (const sub of valMap.get(oldVal[field])) {
253
+ candidates.add(sub);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return candidates;
259
+ }
260
+ addEquality(field, value, sub) {
261
+ if (!this.equality.has(field)) this.equality.set(field, /* @__PURE__ */ new Map());
262
+ const valMap = this.equality.get(field);
263
+ if (!valMap.has(value)) valMap.set(value, /* @__PURE__ */ new Set());
264
+ valMap.get(value).add(sub);
265
+ }
266
+ removeEquality(field, value, sub) {
267
+ const valMap = this.equality.get(field);
268
+ if (valMap) {
269
+ const set = valMap.get(value);
270
+ if (set) {
271
+ set.delete(sub);
272
+ if (set.size === 0) valMap.delete(value);
273
+ }
274
+ if (valMap.size === 0) this.equality.delete(field);
275
+ }
276
+ }
277
+ addInterest(field, sub) {
278
+ if (!this.interest.has(field)) this.interest.set(field, /* @__PURE__ */ new Set());
279
+ this.interest.get(field).add(sub);
280
+ }
281
+ removeInterest(field, sub) {
282
+ const set = this.interest.get(field);
283
+ if (set) {
284
+ set.delete(sub);
285
+ if (set.size === 0) this.interest.delete(field);
286
+ }
287
+ }
288
+ };
289
+ var QueryRegistry = class {
290
+ constructor() {
291
+ // MapName -> Set of Subscriptions (Legacy/Backup)
292
+ this.subscriptions = /* @__PURE__ */ new Map();
293
+ // MapName -> Reverse Index
294
+ this.indexes = /* @__PURE__ */ new Map();
295
+ }
296
+ register(sub) {
297
+ if (!this.subscriptions.has(sub.mapName)) {
298
+ this.subscriptions.set(sub.mapName, /* @__PURE__ */ new Set());
299
+ this.indexes.set(sub.mapName, new ReverseQueryIndex());
300
+ }
301
+ const interestedFields = this.analyzeQueryFields(sub.query);
302
+ sub.interestedFields = interestedFields;
303
+ this.subscriptions.get(sub.mapName).add(sub);
304
+ this.indexes.get(sub.mapName).add(sub);
305
+ logger.info({ clientId: sub.clientId, mapName: sub.mapName, query: sub.query }, "Client subscribed");
306
+ }
307
+ unregister(queryId) {
308
+ for (const [mapName, subs] of this.subscriptions) {
309
+ for (const sub of subs) {
310
+ if (sub.id === queryId) {
311
+ subs.delete(sub);
312
+ this.indexes.get(mapName)?.remove(sub);
313
+ return;
314
+ }
315
+ }
316
+ }
317
+ }
318
+ unsubscribeAll(clientId) {
319
+ for (const [mapName, subs] of this.subscriptions) {
320
+ for (const sub of subs) {
321
+ if (sub.clientId === clientId) {
322
+ subs.delete(sub);
323
+ this.indexes.get(mapName)?.remove(sub);
324
+ }
325
+ }
326
+ }
327
+ }
328
+ /**
329
+ * Refreshes all subscriptions for a given map.
330
+ * Useful when the map is bulk-loaded from storage.
331
+ */
332
+ refreshSubscriptions(mapName, map) {
333
+ const subs = this.subscriptions.get(mapName);
334
+ if (!subs || subs.size === 0) return;
335
+ const allRecords = this.getMapRecords(map);
336
+ for (const sub of subs) {
337
+ const newResults = executeQuery(allRecords, sub.query);
338
+ const newResultKeys = new Set(newResults.map((r) => r.key));
339
+ for (const key of sub.previousResultKeys) {
340
+ if (!newResultKeys.has(key)) {
341
+ this.sendUpdate(sub, key, null, "REMOVE");
342
+ }
343
+ }
344
+ for (const res of newResults) {
345
+ this.sendUpdate(sub, res.key, res.value, "UPDATE");
346
+ }
347
+ sub.previousResultKeys = newResultKeys;
348
+ }
349
+ }
350
+ getMapRecords(map) {
351
+ const recordsMap = /* @__PURE__ */ new Map();
352
+ const mapAny = map;
353
+ if (typeof mapAny.allKeys === "function" && typeof mapAny.getRecord === "function") {
354
+ for (const key of mapAny.allKeys()) {
355
+ const rec = mapAny.getRecord(key);
356
+ if (rec) {
357
+ recordsMap.set(key, rec);
358
+ }
359
+ }
360
+ } else if (mapAny.items instanceof Map && typeof mapAny.get === "function") {
361
+ const items = mapAny.items;
362
+ for (const key of items.keys()) {
363
+ const values = mapAny.get(key);
364
+ if (values.length > 0) {
365
+ recordsMap.set(key, { value: values });
366
+ }
367
+ }
368
+ }
369
+ return recordsMap;
370
+ }
371
+ /**
372
+ * Processes a record change for all relevant subscriptions.
373
+ * Calculates diffs and sends updates.
374
+ */
375
+ processChange(mapName, map, changeKey, changeRecord, oldRecord) {
376
+ const index = this.indexes.get(mapName);
377
+ if (!index) return;
378
+ const newVal = this.extractValue(changeRecord);
379
+ const oldVal = this.extractValue(oldRecord);
380
+ const changedFields = this.getChangedFields(oldVal, newVal);
381
+ if (changedFields !== "ALL" && changedFields.size === 0 && oldRecord && changeRecord) {
382
+ return;
383
+ }
384
+ const candidates = index.getCandidates(changedFields, oldVal, newVal);
385
+ if (candidates.size === 0) return;
386
+ let recordsMap = null;
387
+ const getRecordsMap = () => {
388
+ if (recordsMap) return recordsMap;
389
+ recordsMap = this.getMapRecords(map);
390
+ return recordsMap;
391
+ };
392
+ for (const sub of candidates) {
393
+ const dummyRecord = {
394
+ value: newVal,
395
+ timestamp: { millis: 0, counter: 0, nodeId: "" }
396
+ // Dummy timestamp for matchesQuery
397
+ };
398
+ const isMatch = matchesQuery(dummyRecord, sub.query);
399
+ const wasInResult = sub.previousResultKeys.has(changeKey);
400
+ if (!isMatch && !wasInResult) {
401
+ continue;
402
+ }
403
+ const allRecords = getRecordsMap();
404
+ const newResults = executeQuery(allRecords, sub.query);
405
+ const newResultKeys = new Set(newResults.map((r) => r.key));
406
+ for (const key of sub.previousResultKeys) {
407
+ if (!newResultKeys.has(key)) {
408
+ this.sendUpdate(sub, key, null, "REMOVE");
409
+ }
410
+ }
411
+ for (const res of newResults) {
412
+ const key = res.key;
413
+ const isNew = !sub.previousResultKeys.has(key);
414
+ if (key === changeKey) {
415
+ this.sendUpdate(sub, key, res.value, "UPDATE");
416
+ } else if (isNew) {
417
+ this.sendUpdate(sub, key, res.value, "UPDATE");
418
+ }
419
+ }
420
+ sub.previousResultKeys = newResultKeys;
421
+ }
422
+ }
423
+ extractValue(record) {
424
+ if (!record) return null;
425
+ if (Array.isArray(record)) {
426
+ return record.map((r) => r.value);
427
+ }
428
+ return record.value;
429
+ }
430
+ sendUpdate(sub, key, value, type) {
431
+ if (sub.socket.readyState === 1) {
432
+ sub.socket.send((0, import_core2.serialize)({
433
+ type: "QUERY_UPDATE",
434
+ payload: {
435
+ queryId: sub.id,
436
+ key,
437
+ value,
438
+ type
439
+ }
440
+ }));
441
+ }
442
+ }
443
+ analyzeQueryFields(query) {
444
+ const fields = /* @__PURE__ */ new Set();
445
+ try {
446
+ if (query.predicate) {
447
+ const extract = (node) => {
448
+ if (node.attribute) fields.add(node.attribute);
449
+ if (node.children) node.children.forEach(extract);
450
+ };
451
+ extract(query.predicate);
452
+ }
453
+ if (query.where) {
454
+ Object.keys(query.where).forEach((k) => fields.add(k));
455
+ }
456
+ if (query.sort) {
457
+ Object.keys(query.sort).forEach((k) => fields.add(k));
458
+ }
459
+ } catch (e) {
460
+ return "ALL";
461
+ }
462
+ return fields.size > 0 ? fields : "ALL";
463
+ }
464
+ getChangedFields(oldValue, newValue) {
465
+ if (Array.isArray(oldValue) || Array.isArray(newValue)) return "ALL";
466
+ if (oldValue === newValue) return /* @__PURE__ */ new Set();
467
+ if (!oldValue && !newValue) return /* @__PURE__ */ new Set();
468
+ if (!oldValue) return new Set(Object.keys(newValue || {}));
469
+ if (!newValue) return new Set(Object.keys(oldValue || {}));
470
+ const changes = /* @__PURE__ */ new Set();
471
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(oldValue), ...Object.keys(newValue)]);
472
+ for (const key of allKeys) {
473
+ if (oldValue[key] !== newValue[key]) {
474
+ changes.add(key);
475
+ }
476
+ }
477
+ return changes;
478
+ }
479
+ };
480
+
481
+ // src/topic/TopicManager.ts
482
+ var TopicManager = class {
483
+ // M1: Basic limit
484
+ constructor(config) {
485
+ this.subscribers = /* @__PURE__ */ new Map();
486
+ this.MAX_SUBSCRIPTIONS = 100;
487
+ this.cluster = config.cluster;
488
+ this.sendToClient = config.sendToClient;
489
+ }
490
+ validateTopic(topic) {
491
+ if (!topic || topic.length > 256 || !/^[\w\-.:/]+$/.test(topic)) {
492
+ throw new Error("Invalid topic name");
493
+ }
494
+ }
495
+ /**
496
+ * Subscribe a client to a topic
497
+ */
498
+ subscribe(clientId, topic) {
499
+ this.validateTopic(topic);
500
+ let count = 0;
501
+ for (const subs of this.subscribers.values()) {
502
+ if (subs.has(clientId)) count++;
503
+ }
504
+ if (count >= this.MAX_SUBSCRIPTIONS) {
505
+ throw new Error("Subscription limit reached");
506
+ }
507
+ if (!this.subscribers.has(topic)) {
508
+ this.subscribers.set(topic, /* @__PURE__ */ new Set());
509
+ }
510
+ this.subscribers.get(topic).add(clientId);
511
+ logger.debug({ clientId, topic }, "Client subscribed to topic");
512
+ }
513
+ /**
514
+ * Unsubscribe a client from a topic
515
+ */
516
+ unsubscribe(clientId, topic) {
517
+ const subs = this.subscribers.get(topic);
518
+ if (subs) {
519
+ subs.delete(clientId);
520
+ if (subs.size === 0) {
521
+ this.subscribers.delete(topic);
522
+ }
523
+ logger.debug({ clientId, topic }, "Client unsubscribed from topic");
524
+ }
525
+ }
526
+ /**
527
+ * Clean up all subscriptions for a client (e.g. on disconnect)
528
+ */
529
+ unsubscribeAll(clientId) {
530
+ for (const [topic, subs] of this.subscribers) {
531
+ if (subs.has(clientId)) {
532
+ subs.delete(clientId);
533
+ if (subs.size === 0) {
534
+ this.subscribers.delete(topic);
535
+ }
536
+ }
537
+ }
538
+ }
539
+ /**
540
+ * Publish a message to a topic
541
+ * @param topic Topic name
542
+ * @param data Message data
543
+ * @param senderId Client ID of the publisher (optional)
544
+ * @param fromCluster Whether this message came from another cluster node
545
+ */
546
+ publish(topic, data, senderId, fromCluster = false) {
547
+ this.validateTopic(topic);
548
+ const subs = this.subscribers.get(topic);
549
+ if (subs) {
550
+ const payload = {
551
+ topic,
552
+ data,
553
+ publisherId: senderId,
554
+ timestamp: Date.now()
555
+ };
556
+ const message = {
557
+ type: "TOPIC_MESSAGE",
558
+ payload
559
+ };
560
+ for (const clientId of subs) {
561
+ if (clientId !== senderId) {
562
+ this.sendToClient(clientId, message);
563
+ }
564
+ }
565
+ }
566
+ if (!fromCluster) {
567
+ this.cluster.getMembers().forEach((nodeId) => {
568
+ if (!this.cluster.isLocal(nodeId)) {
569
+ this.cluster.send(nodeId, "CLUSTER_TOPIC_PUB", {
570
+ topic,
571
+ data,
572
+ originalSenderId: senderId
573
+ });
574
+ }
575
+ });
576
+ }
577
+ }
578
+ };
579
+
580
+ // src/cluster/ClusterManager.ts
581
+ var import_ws = require("ws");
582
+ var import_events = require("events");
583
+ var dns = __toESM(require("dns"));
584
+ var import_fs = require("fs");
585
+ var https = __toESM(require("https"));
586
+ var ClusterManager = class extends import_events.EventEmitter {
587
+ constructor(config) {
588
+ super();
589
+ this.members = /* @__PURE__ */ new Map();
590
+ this.pendingConnections = /* @__PURE__ */ new Set();
591
+ this.reconnectIntervals = /* @__PURE__ */ new Map();
592
+ this._actualPort = 0;
593
+ this.config = config;
594
+ }
595
+ /** Get the actual port the cluster is listening on */
596
+ get port() {
597
+ return this._actualPort;
598
+ }
599
+ start() {
600
+ return new Promise((resolve) => {
601
+ logger.info({ port: this.config.port, tls: !!this.config.tls?.enabled }, "Starting Cluster Manager");
602
+ if (this.config.tls?.enabled) {
603
+ const tlsOptions = this.buildClusterTLSOptions();
604
+ const httpsServer = https.createServer(tlsOptions);
605
+ this.server = new import_ws.WebSocketServer({ server: httpsServer });
606
+ httpsServer.listen(this.config.port, () => {
607
+ const addr = httpsServer.address();
608
+ this._actualPort = typeof addr === "object" && addr ? addr.port : this.config.port;
609
+ logger.info({ port: this._actualPort }, "Cluster Manager listening (TLS enabled)");
610
+ this.onServerReady(resolve);
611
+ });
612
+ } else {
613
+ this.server = new import_ws.WebSocketServer({ port: this.config.port });
614
+ this.server.on("listening", () => {
615
+ const addr = this.server.address();
616
+ this._actualPort = typeof addr === "object" && addr ? addr.port : this.config.port;
617
+ logger.info({ port: this._actualPort }, "Cluster Manager listening");
618
+ this.onServerReady(resolve);
619
+ });
620
+ }
621
+ this.server?.on("connection", (ws, req) => {
622
+ logger.info({ remoteAddress: req.socket.remoteAddress }, "Incoming cluster connection");
623
+ this.handleSocket(ws, false);
624
+ });
625
+ });
626
+ }
627
+ /** Called when server is ready - registers self and initiates peer connections */
628
+ onServerReady(resolve) {
629
+ this.members.set(this.config.nodeId, {
630
+ nodeId: this.config.nodeId,
631
+ host: this.config.host,
632
+ port: this._actualPort,
633
+ socket: null,
634
+ isSelf: true
635
+ });
636
+ if (this.config.discovery === "kubernetes" && this.config.serviceName) {
637
+ this.startDiscovery();
638
+ } else {
639
+ this.connectToPeers();
640
+ }
641
+ resolve(this._actualPort);
642
+ }
643
+ stop() {
644
+ logger.info({ port: this.config.port }, "Stopping Cluster Manager");
645
+ for (const timeout of this.reconnectIntervals.values()) {
646
+ clearTimeout(timeout);
647
+ }
648
+ this.reconnectIntervals.clear();
649
+ if (this.discoveryTimer) {
650
+ clearInterval(this.discoveryTimer);
651
+ this.discoveryTimer = void 0;
652
+ }
653
+ this.pendingConnections.clear();
654
+ for (const member of this.members.values()) {
655
+ if (member.socket) {
656
+ member.socket.terminate();
657
+ }
658
+ }
659
+ this.members.clear();
660
+ if (this.server) {
661
+ this.server.close();
662
+ }
663
+ }
664
+ connectToPeers() {
665
+ for (const peer of this.config.peers) {
666
+ this.connectToPeer(peer);
667
+ }
668
+ }
669
+ startDiscovery() {
670
+ const runDiscovery = async () => {
671
+ if (!this.config.serviceName) return;
672
+ try {
673
+ const addresses = await dns.promises.resolve4(this.config.serviceName);
674
+ logger.debug({ addresses, serviceName: this.config.serviceName }, "DNS discovery results");
675
+ for (const ip of addresses) {
676
+ const targetPort = this._actualPort || this.config.port;
677
+ const peerAddress = `${ip}:${targetPort}`;
678
+ this.connectToPeer(peerAddress);
679
+ }
680
+ } catch (err) {
681
+ logger.error({ err: err.message, serviceName: this.config.serviceName }, "DNS discovery failed");
682
+ }
683
+ };
684
+ logger.info({ serviceName: this.config.serviceName }, "Starting Kubernetes DNS discovery");
685
+ runDiscovery();
686
+ this.discoveryTimer = setInterval(runDiscovery, this.config.discoveryInterval || 1e4);
687
+ }
688
+ scheduleReconnect(peerAddress, attempt = 0) {
689
+ if (this.reconnectIntervals.has(peerAddress)) return;
690
+ const delay = Math.min(5e3 * Math.pow(2, attempt), 6e4);
691
+ const timeout = setTimeout(() => {
692
+ this.reconnectIntervals.delete(peerAddress);
693
+ this.connectToPeerWithBackoff(peerAddress, attempt + 1);
694
+ }, delay);
695
+ this.reconnectIntervals.set(peerAddress, timeout);
696
+ }
697
+ // Helper to track attempts
698
+ connectToPeerWithBackoff(peerAddress, attempt) {
699
+ this._connectToPeerInternal(peerAddress, attempt);
700
+ }
701
+ connectToPeer(peerAddress) {
702
+ this._connectToPeerInternal(peerAddress, 0);
703
+ }
704
+ _connectToPeerInternal(peerAddress, attempt) {
705
+ if (this.pendingConnections.has(peerAddress)) return;
706
+ for (const member of this.members.values()) {
707
+ if (`${member.host}:${member.port}` === peerAddress) return;
708
+ }
709
+ logger.info({ peerAddress, attempt, tls: !!this.config.tls?.enabled }, "Connecting to peer");
710
+ this.pendingConnections.add(peerAddress);
711
+ try {
712
+ let ws;
713
+ if (this.config.tls?.enabled) {
714
+ const protocol = "wss://";
715
+ const wsOptions = {
716
+ rejectUnauthorized: this.config.tls.rejectUnauthorized !== false
717
+ };
718
+ if (this.config.tls.certPath && this.config.tls.keyPath) {
719
+ wsOptions.cert = (0, import_fs.readFileSync)(this.config.tls.certPath);
720
+ wsOptions.key = (0, import_fs.readFileSync)(this.config.tls.keyPath);
721
+ if (this.config.tls.passphrase) {
722
+ wsOptions.passphrase = this.config.tls.passphrase;
723
+ }
724
+ }
725
+ if (this.config.tls.caCertPath) {
726
+ wsOptions.ca = (0, import_fs.readFileSync)(this.config.tls.caCertPath);
727
+ }
728
+ ws = new import_ws.WebSocket(`${protocol}${peerAddress}`, wsOptions);
729
+ } else {
730
+ ws = new import_ws.WebSocket(`ws://${peerAddress}`);
731
+ }
732
+ ws.on("open", () => {
733
+ this.pendingConnections.delete(peerAddress);
734
+ logger.info({ peerAddress }, "Connected to peer");
735
+ this.handleSocket(ws, true, peerAddress);
736
+ });
737
+ ws.on("error", (err) => {
738
+ logger.error({ peerAddress, err: err.message }, "Connection error to peer");
739
+ this.pendingConnections.delete(peerAddress);
740
+ this.scheduleReconnect(peerAddress, attempt);
741
+ });
742
+ ws.on("close", () => {
743
+ this.pendingConnections.delete(peerAddress);
744
+ });
745
+ } catch (e) {
746
+ this.pendingConnections.delete(peerAddress);
747
+ this.scheduleReconnect(peerAddress, attempt);
748
+ }
749
+ }
750
+ handleSocket(ws, initiated, peerAddress) {
751
+ const helloMsg = {
752
+ type: "HELLO",
753
+ senderId: this.config.nodeId,
754
+ payload: {
755
+ host: this.config.host,
756
+ port: this._actualPort || this.config.port
757
+ }
758
+ };
759
+ ws.send(JSON.stringify(helloMsg));
760
+ let remoteNodeId = null;
761
+ ws.on("message", (data) => {
762
+ try {
763
+ const msg = JSON.parse(data.toString());
764
+ if (msg.type === "HELLO") {
765
+ remoteNodeId = msg.senderId;
766
+ const { host, port } = msg.payload;
767
+ logger.info({ nodeId: remoteNodeId, host, port }, "Peer identified");
768
+ const myId = this.config.nodeId;
769
+ const otherId = remoteNodeId;
770
+ const initiatorId = initiated ? myId : otherId;
771
+ const receiverId = initiated ? otherId : myId;
772
+ if (this.members.has(remoteNodeId)) {
773
+ logger.warn({ nodeId: remoteNodeId }, "Duplicate valid connection. Replacing.");
774
+ }
775
+ this.members.set(remoteNodeId, {
776
+ nodeId: remoteNodeId,
777
+ host,
778
+ port,
779
+ socket: ws,
780
+ isSelf: false
781
+ });
782
+ this.emit("memberJoined", remoteNodeId);
783
+ } else {
784
+ this.emit("message", msg);
785
+ }
786
+ } catch (err) {
787
+ logger.error({ err }, "Failed to parse cluster message");
788
+ }
789
+ });
790
+ ws.on("close", () => {
791
+ if (remoteNodeId) {
792
+ const current = this.members.get(remoteNodeId);
793
+ if (current && current.socket === ws) {
794
+ logger.info({ nodeId: remoteNodeId }, "Peer disconnected");
795
+ this.members.delete(remoteNodeId);
796
+ this.emit("memberLeft", remoteNodeId);
797
+ if (initiated && peerAddress) {
798
+ this.scheduleReconnect(peerAddress, 0);
799
+ }
800
+ } else {
801
+ }
802
+ }
803
+ });
804
+ }
805
+ send(nodeId, type, payload) {
806
+ const member = this.members.get(nodeId);
807
+ if (member && member.socket && member.socket.readyState === import_ws.WebSocket.OPEN) {
808
+ const msg = {
809
+ type,
810
+ senderId: this.config.nodeId,
811
+ payload
812
+ };
813
+ member.socket.send(JSON.stringify(msg));
814
+ } else {
815
+ logger.warn({ nodeId }, "Cannot send to node: not connected");
816
+ }
817
+ }
818
+ sendToNode(nodeId, message) {
819
+ this.send(nodeId, "OP_FORWARD", message);
820
+ }
821
+ getMembers() {
822
+ return Array.from(this.members.keys());
823
+ }
824
+ isLocal(nodeId) {
825
+ return nodeId === this.config.nodeId;
826
+ }
827
+ buildClusterTLSOptions() {
828
+ const config = this.config.tls;
829
+ const options = {
830
+ cert: (0, import_fs.readFileSync)(config.certPath),
831
+ key: (0, import_fs.readFileSync)(config.keyPath),
832
+ minVersion: config.minVersion || "TLSv1.2"
833
+ };
834
+ if (config.caCertPath) {
835
+ options.ca = (0, import_fs.readFileSync)(config.caCertPath);
836
+ }
837
+ if (config.requireClientCert) {
838
+ options.requestCert = true;
839
+ options.rejectUnauthorized = true;
840
+ }
841
+ if (config.passphrase) {
842
+ options.passphrase = config.passphrase;
843
+ }
844
+ return options;
845
+ }
846
+ };
847
+
848
+ // src/cluster/PartitionService.ts
849
+ var import_core3 = require("@topgunbuild/core");
850
+ var PartitionService = class {
851
+ // Standard Hazelcast default
852
+ constructor(cluster) {
853
+ // partitionId -> { owner, backups }
854
+ this.partitions = /* @__PURE__ */ new Map();
855
+ this.PARTITION_COUNT = 271;
856
+ this.BACKUP_COUNT = 1;
857
+ this.cluster = cluster;
858
+ this.cluster.on("memberJoined", () => this.rebalance());
859
+ this.cluster.on("memberLeft", () => this.rebalance());
860
+ this.rebalance();
861
+ }
862
+ getPartitionId(key) {
863
+ return Math.abs((0, import_core3.hashString)(key)) % this.PARTITION_COUNT;
864
+ }
865
+ getDistribution(key) {
866
+ const pId = this.getPartitionId(key);
867
+ return this.partitions.get(pId) || {
868
+ owner: this.cluster.config.nodeId,
869
+ backups: []
870
+ };
871
+ }
872
+ getOwner(key) {
873
+ return this.getDistribution(key).owner;
874
+ }
875
+ isLocalOwner(key) {
876
+ return this.getOwner(key) === this.cluster.config.nodeId;
877
+ }
878
+ isLocalBackup(key) {
879
+ const dist = this.getDistribution(key);
880
+ return dist.backups.includes(this.cluster.config.nodeId);
881
+ }
882
+ isRelated(key) {
883
+ return this.isLocalOwner(key) || this.isLocalBackup(key);
884
+ }
885
+ rebalance() {
886
+ let allMembers = this.cluster.getMembers().sort();
887
+ if (allMembers.length === 0) {
888
+ allMembers = [this.cluster.config.nodeId];
889
+ }
890
+ logger.info({ memberCount: allMembers.length, members: allMembers }, "Rebalancing partitions");
891
+ for (let i = 0; i < this.PARTITION_COUNT; i++) {
892
+ const ownerIndex = i % allMembers.length;
893
+ const owner = allMembers[ownerIndex];
894
+ const backups = [];
895
+ if (allMembers.length > 1) {
896
+ for (let b = 1; b <= this.BACKUP_COUNT; b++) {
897
+ const backupIndex = (ownerIndex + b) % allMembers.length;
898
+ backups.push(allMembers[backupIndex]);
899
+ }
900
+ }
901
+ this.partitions.set(i, { owner, backups });
902
+ }
903
+ }
904
+ };
905
+
906
+ // src/cluster/LockManager.ts
907
+ var import_events2 = require("events");
908
+ var _LockManager = class _LockManager extends import_events2.EventEmitter {
909
+ // 5 minutes
910
+ constructor() {
911
+ super();
912
+ this.locks = /* @__PURE__ */ new Map();
913
+ this.checkInterval = setInterval(() => this.cleanupExpiredLocks(), 1e3);
914
+ }
915
+ stop() {
916
+ clearInterval(this.checkInterval);
917
+ }
918
+ acquire(name, clientId, requestId, ttl) {
919
+ const safeTtl = Math.max(_LockManager.MIN_TTL, Math.min(ttl || _LockManager.MIN_TTL, _LockManager.MAX_TTL));
920
+ let lock = this.locks.get(name);
921
+ if (!lock) {
922
+ lock = {
923
+ name,
924
+ owner: "",
925
+ fencingToken: 0,
926
+ expiry: 0,
927
+ queue: []
928
+ };
929
+ this.locks.set(name, lock);
930
+ }
931
+ const now = Date.now();
932
+ if (!lock.owner || lock.expiry < now) {
933
+ this.grantLock(lock, clientId, safeTtl);
934
+ return { granted: true, fencingToken: lock.fencingToken };
935
+ }
936
+ if (lock.owner === clientId) {
937
+ lock.expiry = Math.max(lock.expiry, now + safeTtl);
938
+ logger.info({ name, clientId, fencingToken: lock.fencingToken }, "Lock lease extended");
939
+ return { granted: true, fencingToken: lock.fencingToken };
940
+ }
941
+ lock.queue.push({ clientId, requestId, ttl: safeTtl, timestamp: now });
942
+ logger.info({ name, clientId, queueLength: lock.queue.length }, "Lock queued");
943
+ return { granted: false };
944
+ }
945
+ release(name, clientId, fencingToken) {
946
+ const lock = this.locks.get(name);
947
+ if (!lock) return false;
948
+ if (lock.owner !== clientId) {
949
+ logger.warn({ name, clientId, owner: lock.owner }, "Release failed: Not owner");
950
+ return false;
951
+ }
952
+ if (lock.fencingToken !== fencingToken) {
953
+ logger.warn({ name, clientId, sentToken: fencingToken, actualToken: lock.fencingToken }, "Release failed: Token mismatch");
954
+ return false;
955
+ }
956
+ this.processNext(lock);
957
+ return true;
958
+ }
959
+ handleClientDisconnect(clientId) {
960
+ for (const lock of this.locks.values()) {
961
+ if (lock.owner === clientId) {
962
+ logger.info({ name: lock.name, clientId }, "Releasing lock due to disconnect");
963
+ this.processNext(lock);
964
+ } else {
965
+ const initialLen = lock.queue.length;
966
+ lock.queue = lock.queue.filter((req) => req.clientId !== clientId);
967
+ if (lock.queue.length < initialLen) {
968
+ logger.info({ name: lock.name, clientId }, "Removed from lock queue due to disconnect");
969
+ }
970
+ }
971
+ }
972
+ }
973
+ grantLock(lock, clientId, ttl) {
974
+ lock.owner = clientId;
975
+ lock.expiry = Date.now() + ttl;
976
+ lock.fencingToken++;
977
+ logger.info({ name: lock.name, clientId, fencingToken: lock.fencingToken }, "Lock granted");
978
+ }
979
+ processNext(lock) {
980
+ const now = Date.now();
981
+ lock.owner = "";
982
+ lock.expiry = 0;
983
+ while (lock.queue.length > 0) {
984
+ const next = lock.queue.shift();
985
+ this.grantLock(lock, next.clientId, next.ttl);
986
+ this.emit("lockGranted", {
987
+ clientId: next.clientId,
988
+ requestId: next.requestId,
989
+ name: lock.name,
990
+ fencingToken: lock.fencingToken
991
+ });
992
+ return;
993
+ }
994
+ if (lock.queue.length === 0) {
995
+ this.locks.delete(lock.name);
996
+ }
997
+ }
998
+ cleanupExpiredLocks() {
999
+ const now = Date.now();
1000
+ const lockNames = Array.from(this.locks.keys());
1001
+ for (const name of lockNames) {
1002
+ const lock = this.locks.get(name);
1003
+ if (!lock) continue;
1004
+ if (lock.owner && lock.expiry < now) {
1005
+ logger.info({ name: lock.name, owner: lock.owner }, "Lock expired, processing next");
1006
+ this.processNext(lock);
1007
+ } else if (!lock.owner && lock.queue.length === 0) {
1008
+ this.locks.delete(name);
1009
+ }
1010
+ }
1011
+ }
1012
+ };
1013
+ _LockManager.MIN_TTL = 1e3;
1014
+ // 1 second
1015
+ _LockManager.MAX_TTL = 3e5;
1016
+ var LockManager = _LockManager;
1017
+
1018
+ // src/security/SecurityManager.ts
1019
+ var SecurityManager = class {
1020
+ constructor(policies = []) {
1021
+ this.policies = [];
1022
+ this.policies = policies;
1023
+ }
1024
+ addPolicy(policy) {
1025
+ this.policies.push(policy);
1026
+ }
1027
+ checkPermission(principal, mapName, action) {
1028
+ if (principal.roles.includes("ADMIN")) {
1029
+ return true;
1030
+ }
1031
+ if (mapName.startsWith("$sys/")) {
1032
+ logger.warn({ userId: principal.userId, mapName }, "Access Denied: System Map requires ADMIN role");
1033
+ return false;
1034
+ }
1035
+ for (const policy of this.policies) {
1036
+ const hasRole = this.hasRole(principal, policy.role);
1037
+ const matchesMap = this.matchesMap(mapName, policy.mapNamePattern, principal);
1038
+ if (hasRole && matchesMap) {
1039
+ if (policy.actions.includes("ALL") || policy.actions.includes(action)) {
1040
+ return true;
1041
+ }
1042
+ } else {
1043
+ }
1044
+ }
1045
+ logger.warn({
1046
+ userId: principal.userId,
1047
+ roles: principal.roles,
1048
+ mapName,
1049
+ action,
1050
+ policyCount: this.policies.length
1051
+ }, "SecurityManager: Access Denied - No matching policy found");
1052
+ return false;
1053
+ }
1054
+ filterObject(object, principal, mapName) {
1055
+ if (!object || typeof object !== "object") return object;
1056
+ if (principal.roles.includes("ADMIN")) return object;
1057
+ if (Array.isArray(object)) {
1058
+ return object.map((item) => this.filterObject(item, principal, mapName));
1059
+ }
1060
+ let allowedFields = null;
1061
+ let accessGranted = false;
1062
+ for (const policy of this.policies) {
1063
+ if (this.hasRole(principal, policy.role) && this.matchesMap(mapName, policy.mapNamePattern, principal)) {
1064
+ if (policy.actions.includes("ALL") || policy.actions.includes("READ")) {
1065
+ accessGranted = true;
1066
+ if (!policy.allowedFields || policy.allowedFields.length === 0 || policy.allowedFields.includes("*")) {
1067
+ return object;
1068
+ }
1069
+ if (allowedFields === null) allowedFields = /* @__PURE__ */ new Set();
1070
+ policy.allowedFields.forEach((f) => allowedFields.add(f));
1071
+ }
1072
+ }
1073
+ }
1074
+ if (!accessGranted) return null;
1075
+ if (allowedFields === null) return object;
1076
+ const filtered = {};
1077
+ for (const key of Object.keys(object)) {
1078
+ if (allowedFields.has(key)) {
1079
+ filtered[key] = object[key];
1080
+ }
1081
+ }
1082
+ return filtered;
1083
+ }
1084
+ hasRole(principal, role) {
1085
+ return principal.roles.includes(role);
1086
+ }
1087
+ matchesMap(mapName, pattern, principal) {
1088
+ let finalPattern = pattern;
1089
+ if (pattern.includes("{userId}") && principal) {
1090
+ finalPattern = pattern.replace("{userId}", principal.userId);
1091
+ }
1092
+ if (finalPattern === "*") return true;
1093
+ if (finalPattern === mapName) return true;
1094
+ if (finalPattern.endsWith("*")) {
1095
+ const prefix = finalPattern.slice(0, -1);
1096
+ return mapName.startsWith(prefix);
1097
+ }
1098
+ return false;
1099
+ }
1100
+ };
1101
+
1102
+ // src/monitoring/MetricsService.ts
1103
+ var import_prom_client = require("prom-client");
1104
+ var MetricsService = class {
1105
+ constructor() {
1106
+ this.registry = new import_prom_client.Registry();
1107
+ (0, import_prom_client.collectDefaultMetrics)({ register: this.registry, prefix: "topgun_" });
1108
+ this.connectedClients = new import_prom_client.Gauge({
1109
+ name: "topgun_connected_clients",
1110
+ help: "Number of currently connected clients",
1111
+ registers: [this.registry]
1112
+ });
1113
+ this.mapSizeItems = new import_prom_client.Gauge({
1114
+ name: "topgun_map_size_items",
1115
+ help: "Number of items in a map",
1116
+ labelNames: ["map"],
1117
+ registers: [this.registry]
1118
+ });
1119
+ this.opsTotal = new import_prom_client.Counter({
1120
+ name: "topgun_ops_total",
1121
+ help: "Total number of operations",
1122
+ labelNames: ["type", "map"],
1123
+ registers: [this.registry]
1124
+ });
1125
+ this.memoryUsage = new import_prom_client.Gauge({
1126
+ name: "topgun_memory_usage_bytes",
1127
+ help: "Current memory usage in bytes",
1128
+ registers: [this.registry],
1129
+ collect() {
1130
+ this.set(process.memoryUsage().heapUsed);
1131
+ }
1132
+ });
1133
+ this.clusterMembers = new import_prom_client.Gauge({
1134
+ name: "topgun_cluster_members",
1135
+ help: "Number of active cluster members",
1136
+ registers: [this.registry]
1137
+ });
1138
+ }
1139
+ destroy() {
1140
+ this.registry.clear();
1141
+ }
1142
+ setConnectedClients(count) {
1143
+ this.connectedClients.set(count);
1144
+ }
1145
+ setMapSize(mapName, size) {
1146
+ this.mapSizeItems.set({ map: mapName }, size);
1147
+ }
1148
+ incOp(type, mapName) {
1149
+ this.opsTotal.inc({ type, map: mapName });
1150
+ }
1151
+ setClusterMembers(count) {
1152
+ this.clusterMembers.set(count);
1153
+ }
1154
+ async getMetrics() {
1155
+ return this.registry.metrics();
1156
+ }
1157
+ async getMetricsJson() {
1158
+ const metrics = await this.registry.getMetricsAsJSON();
1159
+ const result = {};
1160
+ for (const metric of metrics) {
1161
+ if (metric.values.length === 1) {
1162
+ result[metric.name] = metric.values[0].value;
1163
+ } else {
1164
+ result[metric.name] = metric.values;
1165
+ }
1166
+ }
1167
+ return result;
1168
+ }
1169
+ getContentType() {
1170
+ return this.registry.contentType;
1171
+ }
1172
+ };
1173
+
1174
+ // src/system/SystemManager.ts
1175
+ var SystemManager = class {
1176
+ constructor(cluster, metrics, getMap) {
1177
+ this.cluster = cluster;
1178
+ this.metrics = metrics;
1179
+ this.getMap = getMap;
1180
+ }
1181
+ start() {
1182
+ this.setupClusterMap();
1183
+ this.setupStatsMap();
1184
+ this.setupMapsMap();
1185
+ this.statsInterval = setInterval(() => this.updateStats(), 5e3);
1186
+ this.cluster.on("memberJoined", () => this.updateClusterMap());
1187
+ this.cluster.on("memberLeft", () => this.updateClusterMap());
1188
+ this.updateClusterMap();
1189
+ this.updateStats();
1190
+ }
1191
+ stop() {
1192
+ if (this.statsInterval) {
1193
+ clearInterval(this.statsInterval);
1194
+ }
1195
+ }
1196
+ notifyMapCreated(mapName) {
1197
+ if (mapName.startsWith("$sys/")) return;
1198
+ this.updateMapsMap(mapName);
1199
+ }
1200
+ setupClusterMap() {
1201
+ this.getMap("$sys/cluster");
1202
+ }
1203
+ setupStatsMap() {
1204
+ this.getMap("$sys/stats");
1205
+ }
1206
+ setupMapsMap() {
1207
+ this.getMap("$sys/maps");
1208
+ }
1209
+ updateClusterMap() {
1210
+ try {
1211
+ const map = this.getMap("$sys/cluster");
1212
+ const members = this.cluster.getMembers();
1213
+ for (const memberId of members) {
1214
+ const isLocal = this.cluster.isLocal(memberId);
1215
+ map.set(memberId, {
1216
+ id: memberId,
1217
+ status: "UP",
1218
+ isLocal,
1219
+ lastUpdated: Date.now()
1220
+ });
1221
+ }
1222
+ } catch (err) {
1223
+ logger.error({ err }, "Failed to update $sys/cluster");
1224
+ }
1225
+ }
1226
+ async updateStats() {
1227
+ try {
1228
+ const map = this.getMap("$sys/stats");
1229
+ const metrics = await this.metrics.getMetricsJson();
1230
+ map.set(this.cluster.config.nodeId, {
1231
+ ...metrics,
1232
+ timestamp: Date.now()
1233
+ });
1234
+ } catch (err) {
1235
+ logger.error({ err }, "Failed to update $sys/stats");
1236
+ }
1237
+ }
1238
+ updateMapsMap(mapName) {
1239
+ try {
1240
+ const map = this.getMap("$sys/maps");
1241
+ map.set(mapName, {
1242
+ name: mapName,
1243
+ createdAt: Date.now()
1244
+ });
1245
+ } catch (err) {
1246
+ logger.error({ err }, "Failed to update $sys/maps");
1247
+ }
1248
+ }
1249
+ };
1250
+
1251
+ // src/ServerCoordinator.ts
1252
+ var GC_INTERVAL_MS = 60 * 60 * 1e3;
1253
+ var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
1254
+ var ServerCoordinator = class {
1255
+ constructor(config) {
1256
+ this.clients = /* @__PURE__ */ new Map();
1257
+ // Interceptors
1258
+ this.interceptors = [];
1259
+ // In-memory storage (partitioned later)
1260
+ this.maps = /* @__PURE__ */ new Map();
1261
+ this.pendingClusterQueries = /* @__PURE__ */ new Map();
1262
+ // GC Consensus State
1263
+ this.gcReports = /* @__PURE__ */ new Map();
1264
+ // Track map loading state to avoid returning empty results during async load
1265
+ this.mapLoadingPromises = /* @__PURE__ */ new Map();
1266
+ this._actualPort = 0;
1267
+ this._actualClusterPort = 0;
1268
+ this._readyPromise = new Promise((resolve) => {
1269
+ this._readyResolve = resolve;
1270
+ });
1271
+ this.hlc = new import_core4.HLC(config.nodeId);
1272
+ this.storage = config.storage;
1273
+ const rawSecret = config.jwtSecret || process.env.JWT_SECRET || "topgun-secret-dev";
1274
+ this.jwtSecret = rawSecret.replace(/\\n/g, "\n");
1275
+ this.queryRegistry = new QueryRegistry();
1276
+ this.securityManager = new SecurityManager(config.securityPolicies || []);
1277
+ this.interceptors = config.interceptors || [];
1278
+ this.metricsService = new MetricsService();
1279
+ if (config.tls?.enabled) {
1280
+ const tlsOptions = this.buildTLSOptions(config.tls);
1281
+ this.httpServer = (0, import_https.createServer)(tlsOptions, (_req, res) => {
1282
+ res.writeHead(200);
1283
+ res.end("TopGun Server Running (Secure)");
1284
+ });
1285
+ logger.info("TLS enabled for client connections");
1286
+ } else {
1287
+ this.httpServer = (0, import_http.createServer)((_req, res) => {
1288
+ res.writeHead(200);
1289
+ res.end("TopGun Server Running");
1290
+ });
1291
+ if (process.env.NODE_ENV === "production") {
1292
+ logger.warn("\u26A0\uFE0F TLS is disabled! Client connections are NOT encrypted.");
1293
+ }
1294
+ }
1295
+ const metricsPort = config.metricsPort !== void 0 ? config.metricsPort : 9090;
1296
+ this.metricsServer = (0, import_http.createServer)(async (req, res) => {
1297
+ if (req.url === "/metrics") {
1298
+ try {
1299
+ res.setHeader("Content-Type", this.metricsService.getContentType());
1300
+ res.end(await this.metricsService.getMetrics());
1301
+ } catch (err) {
1302
+ res.statusCode = 500;
1303
+ res.end("Internal Server Error");
1304
+ }
1305
+ } else {
1306
+ res.statusCode = 404;
1307
+ res.end();
1308
+ }
1309
+ });
1310
+ this.metricsServer.listen(metricsPort, () => {
1311
+ logger.info({ port: metricsPort }, "Metrics server listening");
1312
+ });
1313
+ this.metricsServer.on("error", (err) => {
1314
+ logger.error({ err, port: metricsPort }, "Metrics server failed to start");
1315
+ });
1316
+ this.wss = new import_ws2.WebSocketServer({ server: this.httpServer });
1317
+ this.wss.on("connection", (ws) => this.handleConnection(ws));
1318
+ this.httpServer.listen(config.port, () => {
1319
+ const addr = this.httpServer.address();
1320
+ this._actualPort = typeof addr === "object" && addr ? addr.port : config.port;
1321
+ logger.info({ port: this._actualPort }, "Server Coordinator listening");
1322
+ const clusterPort = config.clusterPort ?? 0;
1323
+ const peers = config.resolvePeers ? config.resolvePeers() : config.peers || [];
1324
+ this.cluster = new ClusterManager({
1325
+ nodeId: config.nodeId,
1326
+ host: config.host || "localhost",
1327
+ port: clusterPort,
1328
+ peers,
1329
+ discovery: config.discovery,
1330
+ serviceName: config.serviceName,
1331
+ discoveryInterval: config.discoveryInterval,
1332
+ tls: config.clusterTls
1333
+ });
1334
+ this.partitionService = new PartitionService(this.cluster);
1335
+ this.lockManager = new LockManager();
1336
+ this.lockManager.on("lockGranted", (evt) => this.handleLockGranted(evt));
1337
+ this.topicManager = new TopicManager({
1338
+ cluster: this.cluster,
1339
+ sendToClient: (clientId, message) => {
1340
+ const client = this.clients.get(clientId);
1341
+ if (client && client.socket.readyState === import_ws2.WebSocket.OPEN) {
1342
+ client.socket.send((0, import_core4.serialize)(message));
1343
+ }
1344
+ }
1345
+ });
1346
+ this.systemManager = new SystemManager(
1347
+ this.cluster,
1348
+ this.metricsService,
1349
+ (name) => this.getMap(name)
1350
+ );
1351
+ this.setupClusterListeners();
1352
+ this.cluster.start().then((actualClusterPort) => {
1353
+ this._actualClusterPort = actualClusterPort;
1354
+ this.metricsService.setClusterMembers(this.cluster.getMembers().length);
1355
+ logger.info({ clusterPort: this._actualClusterPort }, "Cluster started");
1356
+ this.systemManager.start();
1357
+ this._readyResolve();
1358
+ }).catch((err) => {
1359
+ this._actualClusterPort = clusterPort;
1360
+ this.metricsService.setClusterMembers(this.cluster.getMembers().length);
1361
+ logger.info({ clusterPort: this._actualClusterPort }, "Cluster started (sync)");
1362
+ this.systemManager.start();
1363
+ this._readyResolve();
1364
+ });
1365
+ });
1366
+ if (this.storage) {
1367
+ this.storage.initialize().then(() => {
1368
+ logger.info("Storage adapter initialized");
1369
+ }).catch((err) => {
1370
+ logger.error({ err }, "Failed to initialize storage");
1371
+ });
1372
+ }
1373
+ this.startGarbageCollection();
1374
+ }
1375
+ /** Wait for server to be fully ready (ports assigned) */
1376
+ ready() {
1377
+ return this._readyPromise;
1378
+ }
1379
+ /** Get the actual port the server is listening on */
1380
+ get port() {
1381
+ return this._actualPort;
1382
+ }
1383
+ /** Get the actual cluster port */
1384
+ get clusterPort() {
1385
+ return this._actualClusterPort;
1386
+ }
1387
+ async shutdown() {
1388
+ logger.info("Shutting down Server Coordinator...");
1389
+ this.httpServer.close();
1390
+ if (this.metricsServer) {
1391
+ this.metricsServer.close();
1392
+ }
1393
+ this.metricsService.destroy();
1394
+ this.wss.close();
1395
+ logger.info(`Closing ${this.clients.size} client connections...`);
1396
+ const shutdownMsg = (0, import_core4.serialize)({ type: "SHUTDOWN_PENDING", retryAfter: 5e3 });
1397
+ for (const client of this.clients.values()) {
1398
+ try {
1399
+ if (client.socket.readyState === import_ws2.WebSocket.OPEN) {
1400
+ client.socket.send(shutdownMsg);
1401
+ client.socket.close(1001, "Server Shutdown");
1402
+ }
1403
+ } catch (e) {
1404
+ logger.error({ err: e, clientId: client.id }, "Error closing client connection");
1405
+ }
1406
+ }
1407
+ this.clients.clear();
1408
+ if (this.cluster) {
1409
+ this.cluster.stop();
1410
+ }
1411
+ if (this.storage) {
1412
+ logger.info("Closing storage connection...");
1413
+ try {
1414
+ await this.storage.close();
1415
+ logger.info("Storage closed successfully.");
1416
+ } catch (err) {
1417
+ logger.error({ err }, "Error closing storage");
1418
+ }
1419
+ }
1420
+ if (this.gcInterval) {
1421
+ clearInterval(this.gcInterval);
1422
+ this.gcInterval = void 0;
1423
+ }
1424
+ if (this.lockManager) {
1425
+ this.lockManager.stop();
1426
+ }
1427
+ if (this.systemManager) {
1428
+ this.systemManager.stop();
1429
+ }
1430
+ logger.info("Server Coordinator shutdown complete.");
1431
+ }
1432
+ async handleConnection(ws) {
1433
+ const clientId = crypto.randomUUID();
1434
+ logger.info({ clientId }, "Client connected (pending auth)");
1435
+ const connection = {
1436
+ id: clientId,
1437
+ socket: ws,
1438
+ isAuthenticated: false,
1439
+ subscriptions: /* @__PURE__ */ new Set(),
1440
+ lastActiveHlc: this.hlc.now()
1441
+ // Initialize with current time
1442
+ };
1443
+ this.clients.set(clientId, connection);
1444
+ this.metricsService.setConnectedClients(this.clients.size);
1445
+ try {
1446
+ const context = {
1447
+ clientId: connection.id,
1448
+ socket: connection.socket,
1449
+ isAuthenticated: connection.isAuthenticated,
1450
+ principal: connection.principal
1451
+ };
1452
+ for (const interceptor of this.interceptors) {
1453
+ if (interceptor.onConnection) {
1454
+ await interceptor.onConnection(context);
1455
+ }
1456
+ }
1457
+ } catch (err) {
1458
+ logger.error({ clientId, err }, "Interceptor rejected connection");
1459
+ ws.close(4e3, "Connection Rejected");
1460
+ this.clients.delete(clientId);
1461
+ return;
1462
+ }
1463
+ ws.on("message", (message) => {
1464
+ try {
1465
+ let data;
1466
+ let buf;
1467
+ if (Buffer.isBuffer(message)) {
1468
+ buf = message;
1469
+ } else if (message instanceof ArrayBuffer) {
1470
+ buf = new Uint8Array(message);
1471
+ } else if (Array.isArray(message)) {
1472
+ buf = Buffer.concat(message);
1473
+ } else {
1474
+ buf = Buffer.from(message);
1475
+ }
1476
+ try {
1477
+ data = (0, import_core4.deserialize)(buf);
1478
+ } catch (e) {
1479
+ try {
1480
+ const text = Buffer.isBuffer(buf) ? buf.toString() : new TextDecoder().decode(buf);
1481
+ data = JSON.parse(text);
1482
+ } catch (jsonErr) {
1483
+ throw e;
1484
+ }
1485
+ }
1486
+ this.handleMessage(connection, data);
1487
+ } catch (err) {
1488
+ logger.error({ err }, "Invalid message format");
1489
+ ws.close(1002, "Protocol Error");
1490
+ }
1491
+ });
1492
+ ws.on("close", () => {
1493
+ logger.info({ clientId }, "Client disconnected");
1494
+ const context = {
1495
+ clientId: connection.id,
1496
+ socket: connection.socket,
1497
+ isAuthenticated: connection.isAuthenticated,
1498
+ principal: connection.principal
1499
+ };
1500
+ for (const interceptor of this.interceptors) {
1501
+ if (interceptor.onDisconnect) {
1502
+ interceptor.onDisconnect(context).catch((err) => {
1503
+ logger.error({ clientId, err }, "Error in onDisconnect interceptor");
1504
+ });
1505
+ }
1506
+ }
1507
+ for (const subId of connection.subscriptions) {
1508
+ this.queryRegistry.unregister(subId);
1509
+ }
1510
+ this.lockManager.handleClientDisconnect(clientId);
1511
+ this.topicManager.unsubscribeAll(clientId);
1512
+ const members = this.cluster.getMembers();
1513
+ for (const memberId of members) {
1514
+ if (!this.cluster.isLocal(memberId)) {
1515
+ this.cluster.send(memberId, "CLUSTER_CLIENT_DISCONNECTED", {
1516
+ originNodeId: this.cluster.config.nodeId,
1517
+ clientId
1518
+ });
1519
+ }
1520
+ }
1521
+ this.clients.delete(clientId);
1522
+ this.metricsService.setConnectedClients(this.clients.size);
1523
+ });
1524
+ ws.send((0, import_core4.serialize)({ type: "AUTH_REQUIRED" }));
1525
+ }
1526
+ async handleMessage(client, rawMessage) {
1527
+ const parseResult = import_core4.MessageSchema.safeParse(rawMessage);
1528
+ if (!parseResult.success) {
1529
+ logger.error({ clientId: client.id, error: parseResult.error }, "Invalid message format from client");
1530
+ client.socket.send((0, import_core4.serialize)({
1531
+ type: "ERROR",
1532
+ payload: { code: 400, message: "Invalid message format", details: parseResult.error.errors }
1533
+ }));
1534
+ return;
1535
+ }
1536
+ const message = parseResult.data;
1537
+ this.updateClientHlc(client, message);
1538
+ if (!client.isAuthenticated) {
1539
+ if (message.type === "AUTH") {
1540
+ const token = message.token;
1541
+ try {
1542
+ const isRSAKey = this.jwtSecret.includes("-----BEGIN");
1543
+ const verifyOptions = isRSAKey ? { algorithms: ["RS256"] } : { algorithms: ["HS256"] };
1544
+ const decoded = jwt.verify(token, this.jwtSecret, verifyOptions);
1545
+ if (!decoded.roles) {
1546
+ decoded.roles = ["USER"];
1547
+ }
1548
+ if (!decoded.userId && decoded.sub) {
1549
+ decoded.userId = decoded.sub;
1550
+ }
1551
+ client.principal = decoded;
1552
+ client.isAuthenticated = true;
1553
+ logger.info({ clientId: client.id, user: client.principal.userId || "anon" }, "Client authenticated");
1554
+ client.socket.send((0, import_core4.serialize)({ type: "AUTH_ACK" }));
1555
+ return;
1556
+ } catch (e) {
1557
+ logger.error({ clientId: client.id, err: e }, "Auth failed");
1558
+ client.socket.send((0, import_core4.serialize)({ type: "AUTH_FAIL", error: "Invalid token" }));
1559
+ client.socket.close(4001, "Unauthorized");
1560
+ }
1561
+ } else {
1562
+ client.socket.close(4001, "Auth required");
1563
+ }
1564
+ return;
1565
+ }
1566
+ switch (message.type) {
1567
+ case "QUERY_SUB": {
1568
+ const { queryId, mapName, query } = message.payload;
1569
+ if (!this.securityManager.checkPermission(client.principal, mapName, "READ")) {
1570
+ logger.warn({ clientId: client.id, mapName }, "Access Denied: QUERY_SUB");
1571
+ client.socket.send((0, import_core4.serialize)({
1572
+ type: "ERROR",
1573
+ payload: { code: 403, message: `Access Denied for map ${mapName}` }
1574
+ }));
1575
+ return;
1576
+ }
1577
+ logger.info({ clientId: client.id, mapName, query }, "Client subscribed");
1578
+ this.metricsService.incOp("SUBSCRIBE", mapName);
1579
+ const allMembers = this.cluster.getMembers();
1580
+ const remoteMembers = allMembers.filter((id) => !this.cluster.isLocal(id));
1581
+ const requestId = crypto.randomUUID();
1582
+ const pending = {
1583
+ requestId,
1584
+ client,
1585
+ queryId,
1586
+ mapName,
1587
+ query,
1588
+ results: [],
1589
+ // Will populate with local results first
1590
+ expectedNodes: new Set(remoteMembers),
1591
+ respondedNodes: /* @__PURE__ */ new Set(),
1592
+ timer: setTimeout(() => this.finalizeClusterQuery(requestId, true), 5e3)
1593
+ // 5s timeout
1594
+ };
1595
+ this.pendingClusterQueries.set(requestId, pending);
1596
+ try {
1597
+ const localResults = await this.executeLocalQuery(mapName, query);
1598
+ pending.results.push(...localResults);
1599
+ if (remoteMembers.length > 0) {
1600
+ for (const nodeId of remoteMembers) {
1601
+ this.cluster.send(nodeId, "CLUSTER_QUERY_EXEC", {
1602
+ requestId,
1603
+ mapName,
1604
+ query
1605
+ });
1606
+ }
1607
+ } else {
1608
+ this.finalizeClusterQuery(requestId);
1609
+ }
1610
+ } catch (err) {
1611
+ logger.error({ err, mapName }, "Failed to execute local query");
1612
+ this.finalizeClusterQuery(requestId);
1613
+ }
1614
+ break;
1615
+ }
1616
+ case "QUERY_UNSUB": {
1617
+ const { queryId: unsubId } = message.payload;
1618
+ this.queryRegistry.unregister(unsubId);
1619
+ client.subscriptions.delete(unsubId);
1620
+ break;
1621
+ }
1622
+ case "CLIENT_OP": {
1623
+ const op = message.payload;
1624
+ const isRemove = op.opType === "REMOVE" || op.record && op.record.value === null;
1625
+ const action = isRemove ? "REMOVE" : "PUT";
1626
+ this.metricsService.incOp(isRemove ? "DELETE" : "PUT", op.mapName);
1627
+ if (!this.securityManager.checkPermission(client.principal, op.mapName, action)) {
1628
+ logger.warn({ clientId: client.id, action, mapName: op.mapName }, "Access Denied: Client OP");
1629
+ client.socket.send((0, import_core4.serialize)({
1630
+ type: "OP_REJECTED",
1631
+ payload: { opId: op.id, reason: "Access Denied" }
1632
+ }));
1633
+ return;
1634
+ }
1635
+ logger.info({ clientId: client.id, opType: op.opType, key: op.key, mapName: op.mapName }, "Received op");
1636
+ if (this.partitionService.isLocalOwner(op.key)) {
1637
+ this.processLocalOp(op, false, client.id).catch((err) => {
1638
+ logger.error({ clientId: client.id, err }, "Op failed");
1639
+ client.socket.send((0, import_core4.serialize)({
1640
+ type: "OP_REJECTED",
1641
+ payload: { opId: op.id, reason: err.message || "Internal Error" }
1642
+ }));
1643
+ });
1644
+ } else {
1645
+ const owner = this.partitionService.getOwner(op.key);
1646
+ logger.info({ key: op.key, owner }, "Forwarding op");
1647
+ this.cluster.sendToNode(owner, op);
1648
+ }
1649
+ break;
1650
+ }
1651
+ case "OP_BATCH": {
1652
+ const ops = message.payload.ops;
1653
+ logger.info({ clientId: client.id, count: ops.length }, "Received batch");
1654
+ let lastProcessedId = null;
1655
+ let rejectedCount = 0;
1656
+ for (const op of ops) {
1657
+ const isRemove = op.opType === "REMOVE" || op.record && op.record.value === null;
1658
+ const action = isRemove ? "REMOVE" : "PUT";
1659
+ if (!this.securityManager.checkPermission(client.principal, op.mapName, action)) {
1660
+ rejectedCount++;
1661
+ logger.warn({ clientId: client.id, action, mapName: op.mapName }, "Access Denied (Batch)");
1662
+ continue;
1663
+ }
1664
+ if (this.partitionService.isLocalOwner(op.key)) {
1665
+ try {
1666
+ await this.processLocalOp({
1667
+ mapName: op.mapName,
1668
+ key: op.key,
1669
+ record: op.record,
1670
+ orRecord: op.orRecord,
1671
+ orTag: op.orTag,
1672
+ opType: op.opType
1673
+ }, false, client.id);
1674
+ if (op.id) {
1675
+ lastProcessedId = op.id;
1676
+ }
1677
+ } catch (err) {
1678
+ rejectedCount++;
1679
+ logger.warn({ clientId: client.id, mapName: op.mapName, err }, "Op rejected in batch");
1680
+ }
1681
+ } else {
1682
+ const owner = this.partitionService.getOwner(op.key);
1683
+ this.cluster.sendToNode(owner, {
1684
+ type: "CLIENT_OP",
1685
+ payload: {
1686
+ mapName: op.mapName,
1687
+ key: op.key,
1688
+ record: op.record,
1689
+ orRecord: op.orRecord,
1690
+ orTag: op.orTag,
1691
+ opType: op.opType
1692
+ }
1693
+ });
1694
+ if (op.id) {
1695
+ lastProcessedId = op.id;
1696
+ }
1697
+ }
1698
+ }
1699
+ if (lastProcessedId !== null) {
1700
+ client.socket.send((0, import_core4.serialize)({
1701
+ type: "OP_ACK",
1702
+ payload: { lastId: lastProcessedId }
1703
+ }));
1704
+ }
1705
+ if (rejectedCount > 0) {
1706
+ client.socket.send((0, import_core4.serialize)({
1707
+ type: "ERROR",
1708
+ payload: { code: 403, message: `Partial batch failure: ${rejectedCount} ops denied` }
1709
+ }));
1710
+ }
1711
+ break;
1712
+ }
1713
+ case "SYNC_INIT": {
1714
+ if (!this.securityManager.checkPermission(client.principal, message.mapName, "READ")) {
1715
+ logger.warn({ clientId: client.id, mapName: message.mapName }, "Access Denied: SYNC_INIT");
1716
+ client.socket.send((0, import_core4.serialize)({
1717
+ type: "ERROR",
1718
+ payload: { code: 403, message: `Access Denied for map ${message.mapName}` }
1719
+ }));
1720
+ return;
1721
+ }
1722
+ const lastSync = message.lastSyncTimestamp || 0;
1723
+ const now = Date.now();
1724
+ if (lastSync > 0 && now - lastSync > GC_AGE_MS) {
1725
+ logger.warn({ clientId: client.id, lastSync, age: now - lastSync }, "Client too old, sending SYNC_RESET_REQUIRED");
1726
+ client.socket.send((0, import_core4.serialize)({
1727
+ type: "SYNC_RESET_REQUIRED",
1728
+ payload: { mapName: message.mapName }
1729
+ }));
1730
+ return;
1731
+ }
1732
+ logger.info({ clientId: client.id, mapName: message.mapName }, "Client requested sync");
1733
+ this.metricsService.incOp("GET", message.mapName);
1734
+ try {
1735
+ const mapForSync = await this.getMapAsync(message.mapName);
1736
+ if (mapForSync instanceof import_core4.LWWMap) {
1737
+ const tree = mapForSync.getMerkleTree();
1738
+ const rootHash = tree.getRootHash();
1739
+ client.socket.send((0, import_core4.serialize)({
1740
+ type: "SYNC_RESP_ROOT",
1741
+ payload: {
1742
+ mapName: message.mapName,
1743
+ rootHash,
1744
+ timestamp: this.hlc.now()
1745
+ }
1746
+ }));
1747
+ } else {
1748
+ logger.warn({ mapName: message.mapName }, "SYNC_INIT requested for ORMap - Not Implemented");
1749
+ client.socket.send((0, import_core4.serialize)({
1750
+ type: "ERROR",
1751
+ payload: { code: 501, message: `Merkle Sync not supported for ORMap ${message.mapName}` }
1752
+ }));
1753
+ }
1754
+ } catch (err) {
1755
+ logger.error({ err, mapName: message.mapName }, "Failed to load map for SYNC_INIT");
1756
+ client.socket.send((0, import_core4.serialize)({
1757
+ type: "ERROR",
1758
+ payload: { code: 500, message: `Failed to load map ${message.mapName}` }
1759
+ }));
1760
+ }
1761
+ break;
1762
+ }
1763
+ case "MERKLE_REQ_BUCKET": {
1764
+ if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "READ")) {
1765
+ client.socket.send((0, import_core4.serialize)({
1766
+ type: "ERROR",
1767
+ payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
1768
+ }));
1769
+ return;
1770
+ }
1771
+ const { mapName, path } = message.payload;
1772
+ try {
1773
+ const mapForBucket = await this.getMapAsync(mapName);
1774
+ if (mapForBucket instanceof import_core4.LWWMap) {
1775
+ const treeForBucket = mapForBucket.getMerkleTree();
1776
+ const buckets = treeForBucket.getBuckets(path);
1777
+ const node = treeForBucket.getNode(path);
1778
+ if (node && node.entries && node.entries.size > 0) {
1779
+ const diffRecords = [];
1780
+ for (const key of node.entries.keys()) {
1781
+ diffRecords.push({ key, record: mapForBucket.getRecord(key) });
1782
+ }
1783
+ client.socket.send((0, import_core4.serialize)({
1784
+ type: "SYNC_RESP_LEAF",
1785
+ payload: { mapName, path, records: diffRecords }
1786
+ }));
1787
+ } else {
1788
+ client.socket.send((0, import_core4.serialize)({
1789
+ type: "SYNC_RESP_BUCKETS",
1790
+ payload: { mapName, path, buckets }
1791
+ }));
1792
+ }
1793
+ }
1794
+ } catch (err) {
1795
+ logger.error({ err, mapName }, "Failed to load map for MERKLE_REQ_BUCKET");
1796
+ }
1797
+ break;
1798
+ }
1799
+ case "LOCK_REQUEST": {
1800
+ const { requestId, name, ttl } = message.payload;
1801
+ if (!this.securityManager.checkPermission(client.principal, name, "PUT")) {
1802
+ client.socket.send((0, import_core4.serialize)({
1803
+ // We don't have LOCK_DENIED type in schema yet?
1804
+ // Using LOCK_RELEASED with success=false as a hack or ERROR.
1805
+ // Ideally ERROR.
1806
+ type: "ERROR",
1807
+ payload: { code: 403, message: `Access Denied for lock ${name}` }
1808
+ }));
1809
+ return;
1810
+ }
1811
+ if (this.partitionService.isLocalOwner(name)) {
1812
+ const result = this.lockManager.acquire(name, client.id, requestId, ttl || 1e4);
1813
+ if (result.granted) {
1814
+ client.socket.send((0, import_core4.serialize)({
1815
+ type: "LOCK_GRANTED",
1816
+ payload: { requestId, name, fencingToken: result.fencingToken }
1817
+ }));
1818
+ }
1819
+ } else {
1820
+ const owner = this.partitionService.getOwner(name);
1821
+ if (!this.cluster.getMembers().includes(owner)) {
1822
+ client.socket.send((0, import_core4.serialize)({
1823
+ type: "ERROR",
1824
+ payload: { code: 503, message: `Lock owner ${owner} is unavailable` }
1825
+ }));
1826
+ return;
1827
+ }
1828
+ this.cluster.send(owner, "CLUSTER_LOCK_REQ", {
1829
+ originNodeId: this.cluster.config.nodeId,
1830
+ clientId: client.id,
1831
+ requestId,
1832
+ name,
1833
+ ttl
1834
+ });
1835
+ }
1836
+ break;
1837
+ }
1838
+ case "LOCK_RELEASE": {
1839
+ const { requestId, name, fencingToken } = message.payload;
1840
+ if (this.partitionService.isLocalOwner(name)) {
1841
+ const success = this.lockManager.release(name, client.id, fencingToken);
1842
+ client.socket.send((0, import_core4.serialize)({
1843
+ type: "LOCK_RELEASED",
1844
+ payload: { requestId, name, success }
1845
+ }));
1846
+ } else {
1847
+ const owner = this.partitionService.getOwner(name);
1848
+ this.cluster.send(owner, "CLUSTER_LOCK_RELEASE", {
1849
+ originNodeId: this.cluster.config.nodeId,
1850
+ clientId: client.id,
1851
+ requestId,
1852
+ name,
1853
+ fencingToken
1854
+ });
1855
+ }
1856
+ break;
1857
+ }
1858
+ case "TOPIC_SUB": {
1859
+ const { topic } = message.payload;
1860
+ if (!this.securityManager.checkPermission(client.principal, `topic:${topic}`, "READ")) {
1861
+ logger.warn({ clientId: client.id, topic }, "Access Denied: TOPIC_SUB");
1862
+ client.socket.send((0, import_core4.serialize)({
1863
+ type: "ERROR",
1864
+ payload: { code: 403, message: `Access Denied for topic ${topic}` }
1865
+ }));
1866
+ return;
1867
+ }
1868
+ try {
1869
+ this.topicManager.subscribe(client.id, topic);
1870
+ } catch (e) {
1871
+ client.socket.send((0, import_core4.serialize)({
1872
+ type: "ERROR",
1873
+ payload: { code: 400, message: e.message }
1874
+ }));
1875
+ }
1876
+ break;
1877
+ }
1878
+ case "TOPIC_UNSUB": {
1879
+ const { topic } = message.payload;
1880
+ this.topicManager.unsubscribe(client.id, topic);
1881
+ break;
1882
+ }
1883
+ case "TOPIC_PUB": {
1884
+ const { topic, data } = message.payload;
1885
+ if (!this.securityManager.checkPermission(client.principal, `topic:${topic}`, "PUT")) {
1886
+ logger.warn({ clientId: client.id, topic }, "Access Denied: TOPIC_PUB");
1887
+ client.socket.send((0, import_core4.serialize)({
1888
+ type: "ERROR",
1889
+ payload: { code: 403, message: `Access Denied for topic ${topic}` }
1890
+ }));
1891
+ return;
1892
+ }
1893
+ try {
1894
+ this.topicManager.publish(topic, data, client.id);
1895
+ } catch (e) {
1896
+ client.socket.send((0, import_core4.serialize)({
1897
+ type: "ERROR",
1898
+ payload: { code: 400, message: e.message }
1899
+ }));
1900
+ }
1901
+ break;
1902
+ }
1903
+ default:
1904
+ logger.warn({ type: message.type }, "Unknown message type");
1905
+ }
1906
+ }
1907
+ updateClientHlc(client, message) {
1908
+ let ts;
1909
+ if (message.type === "CLIENT_OP") {
1910
+ const op = message.payload;
1911
+ if (op.record && op.record.timestamp) {
1912
+ ts = op.record.timestamp;
1913
+ } else if (op.orRecord && op.orRecord.timestamp) {
1914
+ } else if (op.orTag) {
1915
+ try {
1916
+ ts = import_core4.HLC.parse(op.orTag);
1917
+ } catch (e) {
1918
+ }
1919
+ }
1920
+ }
1921
+ if (ts) {
1922
+ this.hlc.update(ts);
1923
+ client.lastActiveHlc = ts;
1924
+ } else {
1925
+ client.lastActiveHlc = this.hlc.now();
1926
+ }
1927
+ }
1928
+ broadcast(message, excludeClientId) {
1929
+ const isServerEvent = message.type === "SERVER_EVENT";
1930
+ if (isServerEvent) {
1931
+ for (const [id, client] of this.clients) {
1932
+ if (id !== excludeClientId && client.socket.readyState === 1 && client.isAuthenticated && client.principal) {
1933
+ const payload = message.payload;
1934
+ const mapName = payload.mapName;
1935
+ const newPayload = { ...payload };
1936
+ if (newPayload.record) {
1937
+ const newVal = this.securityManager.filterObject(newPayload.record.value, client.principal, mapName);
1938
+ newPayload.record = { ...newPayload.record, value: newVal };
1939
+ }
1940
+ if (newPayload.orRecord) {
1941
+ const newVal = this.securityManager.filterObject(newPayload.orRecord.value, client.principal, mapName);
1942
+ newPayload.orRecord = { ...newPayload.orRecord, value: newVal };
1943
+ }
1944
+ client.socket.send((0, import_core4.serialize)({ ...message, payload: newPayload }));
1945
+ }
1946
+ }
1947
+ } else {
1948
+ const msgData = (0, import_core4.serialize)(message);
1949
+ for (const [id, client] of this.clients) {
1950
+ if (id !== excludeClientId && client.socket.readyState === 1) {
1951
+ client.socket.send(msgData);
1952
+ }
1953
+ }
1954
+ }
1955
+ }
1956
+ setupClusterListeners() {
1957
+ this.cluster.on("memberJoined", () => {
1958
+ this.metricsService.setClusterMembers(this.cluster.getMembers().length);
1959
+ });
1960
+ this.cluster.on("memberLeft", () => {
1961
+ this.metricsService.setClusterMembers(this.cluster.getMembers().length);
1962
+ });
1963
+ this.cluster.on("message", (msg) => {
1964
+ switch (msg.type) {
1965
+ case "OP_FORWARD":
1966
+ logger.info({ senderId: msg.senderId }, "Received forwarded op");
1967
+ if (this.partitionService.isLocalOwner(msg.payload.key)) {
1968
+ this.processLocalOp(msg.payload, true, msg.senderId).catch((err) => {
1969
+ logger.error({ err, senderId: msg.senderId }, "Forwarded op failed");
1970
+ });
1971
+ } else {
1972
+ logger.warn({ key: msg.payload.key }, "Received OP_FORWARD but not owner. Dropping.");
1973
+ }
1974
+ break;
1975
+ case "CLUSTER_EVENT":
1976
+ this.handleClusterEvent(msg.payload);
1977
+ break;
1978
+ case "CLUSTER_QUERY_EXEC": {
1979
+ const { requestId, mapName, query } = msg.payload;
1980
+ this.executeLocalQuery(mapName, query).then((results) => {
1981
+ this.cluster.send(msg.senderId, "CLUSTER_QUERY_RESP", {
1982
+ requestId,
1983
+ results
1984
+ });
1985
+ }).catch((err) => {
1986
+ logger.error({ err, mapName }, "Failed to execute cluster query");
1987
+ this.cluster.send(msg.senderId, "CLUSTER_QUERY_RESP", {
1988
+ requestId,
1989
+ results: []
1990
+ });
1991
+ });
1992
+ break;
1993
+ }
1994
+ case "CLUSTER_QUERY_RESP": {
1995
+ const { requestId: reqId, results: remoteResults } = msg.payload;
1996
+ const pendingQuery = this.pendingClusterQueries.get(reqId);
1997
+ if (pendingQuery) {
1998
+ pendingQuery.results.push(...remoteResults);
1999
+ pendingQuery.respondedNodes.add(msg.senderId);
2000
+ if (pendingQuery.respondedNodes.size === pendingQuery.expectedNodes.size) {
2001
+ this.finalizeClusterQuery(reqId);
2002
+ }
2003
+ }
2004
+ break;
2005
+ }
2006
+ case "CLUSTER_GC_REPORT": {
2007
+ this.handleGcReport(msg.senderId, msg.payload.minHlc);
2008
+ break;
2009
+ }
2010
+ case "CLUSTER_GC_COMMIT": {
2011
+ this.performGarbageCollection(msg.payload.safeTimestamp);
2012
+ break;
2013
+ }
2014
+ case "CLUSTER_LOCK_REQ": {
2015
+ const { originNodeId, clientId, requestId, name, ttl } = msg.payload;
2016
+ const compositeId = `${originNodeId}:${clientId}`;
2017
+ const result = this.lockManager.acquire(name, compositeId, requestId, ttl || 1e4);
2018
+ if (result.granted) {
2019
+ this.cluster.send(originNodeId, "CLUSTER_LOCK_GRANTED", {
2020
+ clientId,
2021
+ requestId,
2022
+ name,
2023
+ fencingToken: result.fencingToken
2024
+ });
2025
+ }
2026
+ break;
2027
+ }
2028
+ case "CLUSTER_LOCK_RELEASE": {
2029
+ const { originNodeId, clientId, requestId, name, fencingToken } = msg.payload;
2030
+ const compositeId = `${originNodeId}:${clientId}`;
2031
+ const success = this.lockManager.release(name, compositeId, fencingToken);
2032
+ this.cluster.send(originNodeId, "CLUSTER_LOCK_RELEASED", {
2033
+ clientId,
2034
+ requestId,
2035
+ name,
2036
+ success
2037
+ });
2038
+ break;
2039
+ }
2040
+ case "CLUSTER_LOCK_RELEASED": {
2041
+ const { clientId, requestId, name, success } = msg.payload;
2042
+ const client = this.clients.get(clientId);
2043
+ if (client) {
2044
+ client.socket.send((0, import_core4.serialize)({
2045
+ type: "LOCK_RELEASED",
2046
+ payload: { requestId, name, success }
2047
+ }));
2048
+ }
2049
+ break;
2050
+ }
2051
+ case "CLUSTER_LOCK_GRANTED": {
2052
+ const { clientId, requestId, name, fencingToken } = msg.payload;
2053
+ const client = this.clients.get(clientId);
2054
+ if (client) {
2055
+ client.socket.send((0, import_core4.serialize)({
2056
+ type: "LOCK_GRANTED",
2057
+ payload: { requestId, name, fencingToken }
2058
+ }));
2059
+ }
2060
+ break;
2061
+ }
2062
+ case "CLUSTER_CLIENT_DISCONNECTED": {
2063
+ const { clientId, originNodeId } = msg.payload;
2064
+ const compositeId = `${originNodeId}:${clientId}`;
2065
+ this.lockManager.handleClientDisconnect(compositeId);
2066
+ break;
2067
+ }
2068
+ case "CLUSTER_TOPIC_PUB": {
2069
+ const { topic, data, originalSenderId } = msg.payload;
2070
+ this.topicManager.publish(topic, data, originalSenderId, true);
2071
+ break;
2072
+ }
2073
+ }
2074
+ });
2075
+ }
2076
+ async executeLocalQuery(mapName, query) {
2077
+ const map = await this.getMapAsync(mapName);
2078
+ const records = /* @__PURE__ */ new Map();
2079
+ if (map instanceof import_core4.LWWMap) {
2080
+ for (const key of map.allKeys()) {
2081
+ const rec = map.getRecord(key);
2082
+ if (rec && rec.value !== null) {
2083
+ records.set(key, rec);
2084
+ }
2085
+ }
2086
+ } else if (map instanceof import_core4.ORMap) {
2087
+ const items = map.items;
2088
+ for (const key of items.keys()) {
2089
+ const values = map.get(key);
2090
+ if (values.length > 0) {
2091
+ records.set(key, { value: values });
2092
+ }
2093
+ }
2094
+ }
2095
+ const localQuery = { ...query };
2096
+ delete localQuery.offset;
2097
+ delete localQuery.limit;
2098
+ return executeQuery(records, localQuery);
2099
+ }
2100
+ finalizeClusterQuery(requestId, timeout = false) {
2101
+ const pending = this.pendingClusterQueries.get(requestId);
2102
+ if (!pending) return;
2103
+ if (timeout) {
2104
+ logger.warn({ requestId, responded: pending.respondedNodes.size, expected: pending.expectedNodes.size }, "Query timed out. Returning partial results.");
2105
+ }
2106
+ clearTimeout(pending.timer);
2107
+ this.pendingClusterQueries.delete(requestId);
2108
+ const { client, queryId, mapName, query, results } = pending;
2109
+ const uniqueResults = /* @__PURE__ */ new Map();
2110
+ for (const res of results) {
2111
+ uniqueResults.set(res.key, res);
2112
+ }
2113
+ const finalResults = Array.from(uniqueResults.values());
2114
+ if (query.sort) {
2115
+ finalResults.sort((a, b) => {
2116
+ for (const [field, direction] of Object.entries(query.sort)) {
2117
+ const valA = a.value[field];
2118
+ const valB = b.value[field];
2119
+ if (valA < valB) return direction === "asc" ? -1 : 1;
2120
+ if (valA > valB) return direction === "asc" ? 1 : -1;
2121
+ }
2122
+ return 0;
2123
+ });
2124
+ }
2125
+ const slicedResults = query.offset || query.limit ? finalResults.slice(query.offset || 0, (query.offset || 0) + (query.limit || finalResults.length)) : finalResults;
2126
+ const resultKeys = new Set(slicedResults.map((r) => r.key));
2127
+ const sub = {
2128
+ id: queryId,
2129
+ clientId: client.id,
2130
+ mapName,
2131
+ query,
2132
+ socket: client.socket,
2133
+ previousResultKeys: resultKeys,
2134
+ interestedFields: "ALL"
2135
+ };
2136
+ this.queryRegistry.register(sub);
2137
+ client.subscriptions.add(queryId);
2138
+ const filteredResults = slicedResults.map((res) => {
2139
+ const filteredValue = this.securityManager.filterObject(res.value, client.principal, mapName);
2140
+ return { ...res, value: filteredValue };
2141
+ });
2142
+ client.socket.send((0, import_core4.serialize)({
2143
+ type: "QUERY_RESP",
2144
+ payload: { queryId, results: filteredResults }
2145
+ }));
2146
+ }
2147
+ handleLockGranted({ clientId, requestId, name, fencingToken }) {
2148
+ const client = this.clients.get(clientId);
2149
+ if (client) {
2150
+ client.socket.send((0, import_core4.serialize)({
2151
+ type: "LOCK_GRANTED",
2152
+ payload: { requestId, name, fencingToken }
2153
+ }));
2154
+ return;
2155
+ }
2156
+ const parts = clientId.split(":");
2157
+ if (parts.length === 2) {
2158
+ const [nodeId, realClientId] = parts;
2159
+ if (nodeId !== this.cluster.config.nodeId) {
2160
+ this.cluster.send(nodeId, "CLUSTER_LOCK_GRANTED", {
2161
+ clientId: realClientId,
2162
+ requestId,
2163
+ name,
2164
+ fencingToken
2165
+ });
2166
+ return;
2167
+ }
2168
+ }
2169
+ logger.warn({ clientId, name }, "Lock granted to unknown client");
2170
+ }
2171
+ async processLocalOp(op, fromCluster, originalSenderId) {
2172
+ let context = {
2173
+ clientId: originalSenderId || "unknown",
2174
+ isAuthenticated: false,
2175
+ // We might need to fetch this if local
2176
+ fromCluster,
2177
+ originalSenderId
2178
+ };
2179
+ if (!fromCluster && originalSenderId) {
2180
+ const client = this.clients.get(originalSenderId);
2181
+ if (client) {
2182
+ context = {
2183
+ clientId: client.id,
2184
+ socket: client.socket,
2185
+ isAuthenticated: client.isAuthenticated,
2186
+ principal: client.principal,
2187
+ fromCluster,
2188
+ originalSenderId
2189
+ };
2190
+ }
2191
+ }
2192
+ let currentOp = op;
2193
+ try {
2194
+ for (const interceptor of this.interceptors) {
2195
+ if (interceptor.onBeforeOp) {
2196
+ if (currentOp) {
2197
+ currentOp = await interceptor.onBeforeOp(currentOp, context);
2198
+ if (!currentOp) {
2199
+ logger.debug({ interceptor: interceptor.name, opId: op.id }, "Interceptor silently dropped op");
2200
+ return;
2201
+ }
2202
+ }
2203
+ }
2204
+ }
2205
+ } catch (err) {
2206
+ logger.warn({ err, opId: op.id }, "Interceptor rejected op");
2207
+ throw err;
2208
+ }
2209
+ if (!currentOp) return;
2210
+ op = currentOp;
2211
+ const typeHint = op.opType === "OR_ADD" || op.opType === "OR_REMOVE" ? "OR" : "LWW";
2212
+ const map = this.getMap(op.mapName, typeHint);
2213
+ if (typeHint === "OR" && map instanceof import_core4.LWWMap) {
2214
+ logger.error({ mapName: op.mapName }, "Map type mismatch: LWWMap but received OR op");
2215
+ throw new Error("Map type mismatch: LWWMap but received OR op");
2216
+ }
2217
+ if (typeHint === "LWW" && map instanceof import_core4.ORMap) {
2218
+ logger.error({ mapName: op.mapName }, "Map type mismatch: ORMap but received LWW op");
2219
+ throw new Error("Map type mismatch: ORMap but received LWW op");
2220
+ }
2221
+ let oldRecord;
2222
+ let recordToStore;
2223
+ let tombstonesToStore;
2224
+ const eventPayload = {
2225
+ mapName: op.mapName,
2226
+ key: op.key
2227
+ // Common fields
2228
+ };
2229
+ if (map instanceof import_core4.LWWMap) {
2230
+ oldRecord = map.getRecord(op.key);
2231
+ map.merge(op.key, op.record);
2232
+ recordToStore = op.record;
2233
+ eventPayload.eventType = "UPDATED";
2234
+ eventPayload.record = op.record;
2235
+ } else if (map instanceof import_core4.ORMap) {
2236
+ oldRecord = map.getRecords(op.key);
2237
+ if (op.opType === "OR_ADD") {
2238
+ map.apply(op.key, op.orRecord);
2239
+ eventPayload.eventType = "OR_ADD";
2240
+ eventPayload.orRecord = op.orRecord;
2241
+ recordToStore = {
2242
+ type: "OR",
2243
+ records: map.getRecords(op.key)
2244
+ };
2245
+ } else if (op.opType === "OR_REMOVE") {
2246
+ map.applyTombstone(op.orTag);
2247
+ eventPayload.eventType = "OR_REMOVE";
2248
+ eventPayload.orTag = op.orTag;
2249
+ recordToStore = {
2250
+ type: "OR",
2251
+ records: map.getRecords(op.key)
2252
+ };
2253
+ tombstonesToStore = {
2254
+ type: "OR_TOMBSTONES",
2255
+ tags: map.getTombstones()
2256
+ };
2257
+ }
2258
+ }
2259
+ this.queryRegistry.processChange(op.mapName, map, op.key, op.record || op.orRecord, oldRecord);
2260
+ const mapSize = map instanceof import_core4.ORMap ? map.totalRecords : map.size;
2261
+ this.metricsService.setMapSize(op.mapName, mapSize);
2262
+ if (this.storage) {
2263
+ if (recordToStore) {
2264
+ this.storage.store(op.mapName, op.key, recordToStore).catch((err) => {
2265
+ logger.error({ mapName: op.mapName, key: op.key, err }, "Failed to persist op");
2266
+ });
2267
+ }
2268
+ if (tombstonesToStore) {
2269
+ this.storage.store(op.mapName, "__tombstones__", tombstonesToStore).catch((err) => {
2270
+ logger.error({ mapName: op.mapName, err }, "Failed to persist tombstones");
2271
+ });
2272
+ }
2273
+ }
2274
+ this.broadcast({
2275
+ type: "SERVER_EVENT",
2276
+ payload: eventPayload,
2277
+ timestamp: this.hlc.now()
2278
+ }, originalSenderId);
2279
+ const members = this.cluster.getMembers();
2280
+ for (const memberId of members) {
2281
+ if (!this.cluster.isLocal(memberId)) {
2282
+ this.cluster.send(memberId, "CLUSTER_EVENT", eventPayload);
2283
+ }
2284
+ }
2285
+ for (const interceptor of this.interceptors) {
2286
+ if (interceptor.onAfterOp) {
2287
+ interceptor.onAfterOp(op, context).catch((err) => {
2288
+ logger.error({ err }, "Error in onAfterOp");
2289
+ });
2290
+ }
2291
+ }
2292
+ }
2293
+ handleClusterEvent(payload) {
2294
+ const { mapName, key, eventType } = payload;
2295
+ const map = this.getMap(mapName, eventType === "OR_ADD" || eventType === "OR_REMOVE" ? "OR" : "LWW");
2296
+ const oldRecord = map instanceof import_core4.LWWMap ? map.getRecord(key) : null;
2297
+ if (this.partitionService.isRelated(key)) {
2298
+ if (map instanceof import_core4.LWWMap && payload.record) {
2299
+ map.merge(key, payload.record);
2300
+ } else if (map instanceof import_core4.ORMap) {
2301
+ if (eventType === "OR_ADD" && payload.orRecord) {
2302
+ map.apply(key, payload.orRecord);
2303
+ } else if (eventType === "OR_REMOVE" && payload.orTag) {
2304
+ map.applyTombstone(payload.orTag);
2305
+ }
2306
+ }
2307
+ }
2308
+ this.queryRegistry.processChange(mapName, map, key, payload.record || payload.orRecord, oldRecord);
2309
+ this.broadcast({
2310
+ type: "SERVER_EVENT",
2311
+ payload,
2312
+ timestamp: this.hlc.now()
2313
+ });
2314
+ }
2315
+ getMap(name, typeHint = "LWW") {
2316
+ if (!this.maps.has(name)) {
2317
+ let map;
2318
+ if (typeHint === "OR") {
2319
+ map = new import_core4.ORMap(this.hlc);
2320
+ } else {
2321
+ map = new import_core4.LWWMap(this.hlc);
2322
+ }
2323
+ this.maps.set(name, map);
2324
+ if (this.storage) {
2325
+ logger.info({ mapName: name }, "Loading map from storage...");
2326
+ const loadPromise = this.loadMapFromStorage(name, typeHint);
2327
+ this.mapLoadingPromises.set(name, loadPromise);
2328
+ loadPromise.finally(() => {
2329
+ this.mapLoadingPromises.delete(name);
2330
+ });
2331
+ }
2332
+ }
2333
+ return this.maps.get(name);
2334
+ }
2335
+ /**
2336
+ * Returns map after ensuring it's fully loaded from storage.
2337
+ * Use this for queries to avoid returning empty results during initial load.
2338
+ */
2339
+ async getMapAsync(name, typeHint = "LWW") {
2340
+ const mapExisted = this.maps.has(name);
2341
+ this.getMap(name, typeHint);
2342
+ const loadingPromise = this.mapLoadingPromises.get(name);
2343
+ const map = this.maps.get(name);
2344
+ const mapSize = map instanceof import_core4.LWWMap ? Array.from(map.entries()).length : map instanceof import_core4.ORMap ? map.size : 0;
2345
+ logger.info({
2346
+ mapName: name,
2347
+ mapExisted,
2348
+ hasLoadingPromise: !!loadingPromise,
2349
+ currentMapSize: mapSize
2350
+ }, "[getMapAsync] State check");
2351
+ if (loadingPromise) {
2352
+ logger.info({ mapName: name }, "[getMapAsync] Waiting for loadMapFromStorage...");
2353
+ await loadingPromise;
2354
+ const newMapSize = map instanceof import_core4.LWWMap ? Array.from(map.entries()).length : map instanceof import_core4.ORMap ? map.size : 0;
2355
+ logger.info({ mapName: name, mapSizeAfterLoad: newMapSize }, "[getMapAsync] Load completed");
2356
+ }
2357
+ return this.maps.get(name);
2358
+ }
2359
+ async loadMapFromStorage(name, typeHint) {
2360
+ try {
2361
+ const keys = await this.storage.loadAllKeys(name);
2362
+ if (keys.length === 0) return;
2363
+ const hasTombstones = keys.includes("__tombstones__");
2364
+ const relatedKeys = keys.filter((k) => this.partitionService.isRelated(k));
2365
+ if (relatedKeys.length === 0) return;
2366
+ const records = await this.storage.loadAll(name, relatedKeys);
2367
+ let count = 0;
2368
+ let isOR = hasTombstones;
2369
+ if (!isOR) {
2370
+ for (const [k, v] of records) {
2371
+ if (k !== "__tombstones__" && v.type === "OR") {
2372
+ isOR = true;
2373
+ break;
2374
+ }
2375
+ }
2376
+ }
2377
+ const currentMap = this.maps.get(name);
2378
+ if (!currentMap) return;
2379
+ let targetMap = currentMap;
2380
+ if (isOR && currentMap instanceof import_core4.LWWMap) {
2381
+ logger.info({ mapName: name }, "Map auto-detected as ORMap. Switching type.");
2382
+ targetMap = new import_core4.ORMap(this.hlc);
2383
+ this.maps.set(name, targetMap);
2384
+ } else if (!isOR && currentMap instanceof import_core4.ORMap && typeHint !== "OR") {
2385
+ logger.info({ mapName: name }, "Map auto-detected as LWWMap. Switching type.");
2386
+ targetMap = new import_core4.LWWMap(this.hlc);
2387
+ this.maps.set(name, targetMap);
2388
+ }
2389
+ if (targetMap instanceof import_core4.ORMap) {
2390
+ for (const [key, record] of records) {
2391
+ if (key === "__tombstones__") {
2392
+ const t = record;
2393
+ if (t && t.tags) t.tags.forEach((tag) => targetMap.applyTombstone(tag));
2394
+ } else {
2395
+ const orVal = record;
2396
+ if (orVal && orVal.records) {
2397
+ orVal.records.forEach((r) => targetMap.apply(key, r));
2398
+ count++;
2399
+ }
2400
+ }
2401
+ }
2402
+ } else if (targetMap instanceof import_core4.LWWMap) {
2403
+ for (const [key, record] of records) {
2404
+ if (!record.type) {
2405
+ targetMap.merge(key, record);
2406
+ count++;
2407
+ }
2408
+ }
2409
+ }
2410
+ if (count > 0) {
2411
+ logger.info({ mapName: name, count }, "Loaded records for map");
2412
+ this.queryRegistry.refreshSubscriptions(name, targetMap);
2413
+ const mapSize = targetMap instanceof import_core4.ORMap ? targetMap.totalRecords : targetMap.size;
2414
+ this.metricsService.setMapSize(name, mapSize);
2415
+ }
2416
+ } catch (err) {
2417
+ logger.error({ mapName: name, err }, "Failed to load map");
2418
+ }
2419
+ }
2420
+ startGarbageCollection() {
2421
+ this.gcInterval = setInterval(() => {
2422
+ this.reportLocalHlc();
2423
+ }, GC_INTERVAL_MS);
2424
+ }
2425
+ reportLocalHlc() {
2426
+ let minHlc = this.hlc.now();
2427
+ for (const client of this.clients.values()) {
2428
+ if (import_core4.HLC.compare(client.lastActiveHlc, minHlc) < 0) {
2429
+ minHlc = client.lastActiveHlc;
2430
+ }
2431
+ }
2432
+ const members = this.cluster.getMembers().sort();
2433
+ const leaderId = members[0];
2434
+ const myId = this.cluster.config.nodeId;
2435
+ if (leaderId === myId) {
2436
+ this.handleGcReport(myId, minHlc);
2437
+ } else {
2438
+ this.cluster.send(leaderId, "CLUSTER_GC_REPORT", { minHlc });
2439
+ }
2440
+ }
2441
+ handleGcReport(nodeId, minHlc) {
2442
+ this.gcReports.set(nodeId, minHlc);
2443
+ const members = this.cluster.getMembers();
2444
+ const allReported = members.every((m) => this.gcReports.has(m));
2445
+ if (allReported) {
2446
+ let globalSafe = this.hlc.now();
2447
+ let initialized = false;
2448
+ for (const ts of this.gcReports.values()) {
2449
+ if (!initialized || import_core4.HLC.compare(ts, globalSafe) < 0) {
2450
+ globalSafe = ts;
2451
+ initialized = true;
2452
+ }
2453
+ }
2454
+ const olderThanMillis = globalSafe.millis - GC_AGE_MS;
2455
+ const safeTimestamp = {
2456
+ millis: olderThanMillis,
2457
+ counter: 0,
2458
+ nodeId: globalSafe.nodeId
2459
+ // Doesn't matter much for comparison if millis match, but best effort
2460
+ };
2461
+ logger.info({
2462
+ globalMinHlc: globalSafe.millis,
2463
+ safeGcTimestamp: olderThanMillis,
2464
+ reportsCount: this.gcReports.size
2465
+ }, "GC Consensus Reached. Broadcasting Commit.");
2466
+ const commitMsg = {
2467
+ type: "CLUSTER_GC_COMMIT",
2468
+ // Handled by cluster listener
2469
+ payload: { safeTimestamp }
2470
+ };
2471
+ for (const member of members) {
2472
+ if (!this.cluster.isLocal(member)) {
2473
+ this.cluster.send(member, "CLUSTER_GC_COMMIT", { safeTimestamp });
2474
+ }
2475
+ }
2476
+ this.performGarbageCollection(safeTimestamp);
2477
+ this.gcReports.clear();
2478
+ }
2479
+ }
2480
+ performGarbageCollection(olderThan) {
2481
+ logger.info({ olderThanMillis: olderThan.millis }, "Performing Garbage Collection");
2482
+ const now = Date.now();
2483
+ for (const [name, map] of this.maps) {
2484
+ if (map instanceof import_core4.LWWMap) {
2485
+ for (const key of map.allKeys()) {
2486
+ const record = map.getRecord(key);
2487
+ if (record && record.value !== null && record.ttlMs) {
2488
+ const expirationTime = record.timestamp.millis + record.ttlMs;
2489
+ if (expirationTime < now) {
2490
+ logger.info({ mapName: name, key }, "Record expired (TTL). Converting to tombstone.");
2491
+ const tombstoneTimestamp = {
2492
+ millis: expirationTime,
2493
+ counter: 0,
2494
+ // Reset counter for expiration time
2495
+ nodeId: this.hlc.getNodeId
2496
+ // Use our ID
2497
+ };
2498
+ const tombstone = { value: null, timestamp: tombstoneTimestamp };
2499
+ const changed = map.merge(key, tombstone);
2500
+ if (changed) {
2501
+ if (this.storage) {
2502
+ this.storage.store(name, key, tombstone).catch(
2503
+ (err) => logger.error({ mapName: name, key, err }, "Failed to persist expired tombstone")
2504
+ );
2505
+ }
2506
+ const eventPayload = {
2507
+ mapName: name,
2508
+ key,
2509
+ eventType: "UPDATED",
2510
+ record: tombstone
2511
+ };
2512
+ this.broadcast({
2513
+ type: "SERVER_EVENT",
2514
+ payload: eventPayload,
2515
+ timestamp: this.hlc.now()
2516
+ });
2517
+ const members = this.cluster.getMembers();
2518
+ for (const memberId of members) {
2519
+ if (!this.cluster.isLocal(memberId)) {
2520
+ this.cluster.send(memberId, "CLUSTER_EVENT", eventPayload);
2521
+ }
2522
+ }
2523
+ }
2524
+ }
2525
+ }
2526
+ }
2527
+ const removedKeys = map.prune(olderThan);
2528
+ if (removedKeys.length > 0) {
2529
+ logger.info({ mapName: name, count: removedKeys.length }, "Pruned records from LWW map");
2530
+ if (this.storage) {
2531
+ this.storage.deleteAll(name, removedKeys).catch((err) => {
2532
+ logger.error({ mapName: name, err }, "Failed to delete pruned keys from storage");
2533
+ });
2534
+ }
2535
+ }
2536
+ } else if (map instanceof import_core4.ORMap) {
2537
+ const items = map.items;
2538
+ const tombstonesSet = map.tombstones;
2539
+ const tagsToExpire = [];
2540
+ for (const [key, keyMap] of items) {
2541
+ for (const [tag, record] of keyMap) {
2542
+ if (!tombstonesSet.has(tag)) {
2543
+ if (record.ttlMs) {
2544
+ const expirationTime = record.timestamp.millis + record.ttlMs;
2545
+ if (expirationTime < now) {
2546
+ tagsToExpire.push({ key, tag });
2547
+ }
2548
+ }
2549
+ }
2550
+ }
2551
+ }
2552
+ for (const { key, tag } of tagsToExpire) {
2553
+ logger.info({ mapName: name, key, tag }, "ORMap Record expired (TTL). Removing.");
2554
+ map.applyTombstone(tag);
2555
+ if (this.storage) {
2556
+ const records = map.getRecords(key);
2557
+ if (records.length > 0) {
2558
+ this.storage.store(name, key, { type: "OR", records });
2559
+ } else {
2560
+ this.storage.delete(name, key);
2561
+ }
2562
+ const currentTombstones = map.getTombstones();
2563
+ this.storage.store(name, "__tombstones__", {
2564
+ type: "OR_TOMBSTONES",
2565
+ tags: currentTombstones
2566
+ });
2567
+ }
2568
+ const eventPayload = {
2569
+ mapName: name,
2570
+ key,
2571
+ eventType: "OR_REMOVE",
2572
+ orTag: tag
2573
+ };
2574
+ this.broadcast({
2575
+ type: "SERVER_EVENT",
2576
+ payload: eventPayload,
2577
+ timestamp: this.hlc.now()
2578
+ });
2579
+ const members = this.cluster.getMembers();
2580
+ for (const memberId of members) {
2581
+ if (!this.cluster.isLocal(memberId)) {
2582
+ this.cluster.send(memberId, "CLUSTER_EVENT", eventPayload);
2583
+ }
2584
+ }
2585
+ }
2586
+ const removedTags = map.prune(olderThan);
2587
+ if (removedTags.length > 0) {
2588
+ logger.info({ mapName: name, count: removedTags.length }, "Pruned tombstones from OR map");
2589
+ if (this.storage) {
2590
+ const currentTombstones = map.getTombstones();
2591
+ this.storage.store(name, "__tombstones__", {
2592
+ type: "OR_TOMBSTONES",
2593
+ tags: currentTombstones
2594
+ }).catch((err) => {
2595
+ logger.error({ mapName: name, err }, "Failed to update tombstones");
2596
+ });
2597
+ }
2598
+ }
2599
+ }
2600
+ }
2601
+ this.broadcast({
2602
+ type: "GC_PRUNE",
2603
+ payload: {
2604
+ olderThan
2605
+ }
2606
+ });
2607
+ }
2608
+ buildTLSOptions(config) {
2609
+ const options = {
2610
+ cert: (0, import_fs2.readFileSync)(config.certPath),
2611
+ key: (0, import_fs2.readFileSync)(config.keyPath),
2612
+ minVersion: config.minVersion || "TLSv1.2"
2613
+ };
2614
+ if (config.caCertPath) {
2615
+ options.ca = (0, import_fs2.readFileSync)(config.caCertPath);
2616
+ }
2617
+ if (config.ciphers) {
2618
+ options.ciphers = config.ciphers;
2619
+ }
2620
+ if (config.passphrase) {
2621
+ options.passphrase = config.passphrase;
2622
+ }
2623
+ return options;
2624
+ }
2625
+ };
2626
+
2627
+ // src/storage/PostgresAdapter.ts
2628
+ var import_pg = require("pg");
2629
+ var DEFAULT_TABLE_NAME = "topgun_maps";
2630
+ var TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
2631
+ function validateTableName(name) {
2632
+ if (!TABLE_NAME_REGEX.test(name)) {
2633
+ throw new Error(
2634
+ `Invalid table name "${name}". Table name must start with a letter or underscore and contain only alphanumeric characters and underscores.`
2635
+ );
2636
+ }
2637
+ }
2638
+ var PostgresAdapter = class {
2639
+ constructor(configOrPool, options) {
2640
+ if (configOrPool instanceof import_pg.Pool || configOrPool.connect) {
2641
+ this.pool = configOrPool;
2642
+ } else {
2643
+ this.pool = new import_pg.Pool(configOrPool);
2644
+ }
2645
+ const tableName = options?.tableName ?? DEFAULT_TABLE_NAME;
2646
+ validateTableName(tableName);
2647
+ this.tableName = tableName;
2648
+ }
2649
+ async initialize() {
2650
+ const client = await this.pool.connect();
2651
+ try {
2652
+ await client.query(`
2653
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
2654
+ map_name TEXT NOT NULL,
2655
+ key TEXT NOT NULL,
2656
+ value JSONB,
2657
+ ts_millis BIGINT NOT NULL,
2658
+ ts_counter INTEGER NOT NULL,
2659
+ ts_node_id TEXT NOT NULL,
2660
+ is_deleted BOOLEAN DEFAULT FALSE,
2661
+ PRIMARY KEY (map_name, key)
2662
+ );
2663
+ `);
2664
+ } finally {
2665
+ client.release();
2666
+ }
2667
+ }
2668
+ async close() {
2669
+ await this.pool.end();
2670
+ }
2671
+ async load(mapName, key) {
2672
+ const res = await this.pool.query(
2673
+ `SELECT value, ts_millis, ts_counter, ts_node_id, is_deleted
2674
+ FROM ${this.tableName}
2675
+ WHERE map_name = $1 AND key = $2`,
2676
+ [mapName, key]
2677
+ );
2678
+ if (res.rows.length === 0) return void 0;
2679
+ const row = res.rows[0];
2680
+ return this.mapRowToRecord(row);
2681
+ }
2682
+ async loadAll(mapName, keys) {
2683
+ const result = /* @__PURE__ */ new Map();
2684
+ if (keys.length === 0) return result;
2685
+ const res = await this.pool.query(
2686
+ `SELECT key, value, ts_millis, ts_counter, ts_node_id, is_deleted
2687
+ FROM ${this.tableName}
2688
+ WHERE map_name = $1 AND key = ANY($2)`,
2689
+ [mapName, keys]
2690
+ );
2691
+ for (const row of res.rows) {
2692
+ result.set(row.key, this.mapRowToRecord(row));
2693
+ }
2694
+ return result;
2695
+ }
2696
+ async loadAllKeys(mapName) {
2697
+ const res = await this.pool.query(
2698
+ `SELECT key FROM ${this.tableName} WHERE map_name = $1`,
2699
+ [mapName]
2700
+ );
2701
+ return res.rows.map((row) => row.key);
2702
+ }
2703
+ async store(mapName, key, record) {
2704
+ let value;
2705
+ let tsMillis;
2706
+ let tsCounter;
2707
+ let tsNodeId;
2708
+ let isDeleted;
2709
+ if (this.isORMapValue(record)) {
2710
+ value = record;
2711
+ tsMillis = 0;
2712
+ tsCounter = 0;
2713
+ tsNodeId = "__ORMAP__";
2714
+ isDeleted = false;
2715
+ } else {
2716
+ const lww = record;
2717
+ value = lww.value;
2718
+ tsMillis = lww.timestamp.millis;
2719
+ tsCounter = lww.timestamp.counter;
2720
+ tsNodeId = lww.timestamp.nodeId;
2721
+ isDeleted = lww.value === null;
2722
+ }
2723
+ await this.pool.query(
2724
+ `INSERT INTO ${this.tableName} (map_name, key, value, ts_millis, ts_counter, ts_node_id, is_deleted)
2725
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
2726
+ ON CONFLICT (map_name, key) DO UPDATE SET
2727
+ value = EXCLUDED.value,
2728
+ ts_millis = EXCLUDED.ts_millis,
2729
+ ts_counter = EXCLUDED.ts_counter,
2730
+ ts_node_id = EXCLUDED.ts_node_id,
2731
+ is_deleted = EXCLUDED.is_deleted`,
2732
+ [
2733
+ mapName,
2734
+ key,
2735
+ JSON.stringify(value),
2736
+ tsMillis,
2737
+ tsCounter,
2738
+ tsNodeId,
2739
+ isDeleted
2740
+ ]
2741
+ );
2742
+ }
2743
+ async storeAll(mapName, records) {
2744
+ const client = await this.pool.connect();
2745
+ try {
2746
+ await client.query("BEGIN");
2747
+ for (const [key, record] of records) {
2748
+ await this.store(mapName, key, record);
2749
+ }
2750
+ await client.query("COMMIT");
2751
+ } catch (e) {
2752
+ await client.query("ROLLBACK");
2753
+ throw e;
2754
+ } finally {
2755
+ client.release();
2756
+ }
2757
+ }
2758
+ async delete(mapName, key) {
2759
+ await this.pool.query(`DELETE FROM ${this.tableName} WHERE map_name = $1 AND key = $2`, [mapName, key]);
2760
+ }
2761
+ async deleteAll(mapName, keys) {
2762
+ if (keys.length === 0) return;
2763
+ await this.pool.query(
2764
+ `DELETE FROM ${this.tableName} WHERE map_name = $1 AND key = ANY($2)`,
2765
+ [mapName, keys]
2766
+ );
2767
+ }
2768
+ mapRowToRecord(row) {
2769
+ if (row.ts_node_id === "__ORMAP__") {
2770
+ return row.value;
2771
+ }
2772
+ return {
2773
+ value: row.is_deleted ? null : row.value,
2774
+ timestamp: {
2775
+ millis: Number(row.ts_millis),
2776
+ counter: row.ts_counter,
2777
+ nodeId: row.ts_node_id
2778
+ }
2779
+ };
2780
+ }
2781
+ isORMapValue(record) {
2782
+ return record && typeof record === "object" && (record.type === "OR" || record.type === "OR_TOMBSTONES");
2783
+ }
2784
+ };
2785
+
2786
+ // src/storage/MemoryServerAdapter.ts
2787
+ var MemoryServerAdapter = class {
2788
+ constructor() {
2789
+ // Map<mapName, Map<key, value>>
2790
+ this.storage = /* @__PURE__ */ new Map();
2791
+ }
2792
+ async initialize() {
2793
+ console.log("[MemoryServerAdapter] Initialized in-memory storage");
2794
+ }
2795
+ async close() {
2796
+ this.storage.clear();
2797
+ console.log("[MemoryServerAdapter] Storage cleared and closed");
2798
+ }
2799
+ getMap(mapName) {
2800
+ let map = this.storage.get(mapName);
2801
+ if (!map) {
2802
+ map = /* @__PURE__ */ new Map();
2803
+ this.storage.set(mapName, map);
2804
+ }
2805
+ return map;
2806
+ }
2807
+ async load(mapName, key) {
2808
+ return this.getMap(mapName).get(key);
2809
+ }
2810
+ async loadAll(mapName, keys) {
2811
+ const map = this.getMap(mapName);
2812
+ const result = /* @__PURE__ */ new Map();
2813
+ for (const key of keys) {
2814
+ const value = map.get(key);
2815
+ if (value !== void 0) {
2816
+ result.set(key, value);
2817
+ }
2818
+ }
2819
+ return result;
2820
+ }
2821
+ async loadAllKeys(mapName) {
2822
+ return Array.from(this.getMap(mapName).keys());
2823
+ }
2824
+ async store(mapName, key, record) {
2825
+ this.getMap(mapName).set(key, record);
2826
+ }
2827
+ async storeAll(mapName, records) {
2828
+ const map = this.getMap(mapName);
2829
+ for (const [key, value] of records) {
2830
+ map.set(key, value);
2831
+ }
2832
+ }
2833
+ async delete(mapName, key) {
2834
+ this.getMap(mapName).delete(key);
2835
+ }
2836
+ async deleteAll(mapName, keys) {
2837
+ const map = this.getMap(mapName);
2838
+ for (const key of keys) {
2839
+ map.delete(key);
2840
+ }
2841
+ }
2842
+ };
2843
+
2844
+ // src/interceptor/TimestampInterceptor.ts
2845
+ var TimestampInterceptor = class {
2846
+ constructor() {
2847
+ this.name = "TimestampInterceptor";
2848
+ }
2849
+ async onBeforeOp(op, context) {
2850
+ if (op.opType === "PUT" && op.record && op.record.value) {
2851
+ if (typeof op.record.value === "object" && op.record.value !== null && !Array.isArray(op.record.value)) {
2852
+ const newValue = {
2853
+ ...op.record.value,
2854
+ _serverTimestamp: Date.now()
2855
+ };
2856
+ logger.debug({ key: op.key, mapName: op.mapName, interceptor: this.name }, "Added timestamp");
2857
+ return {
2858
+ ...op,
2859
+ record: {
2860
+ ...op.record,
2861
+ value: newValue
2862
+ }
2863
+ };
2864
+ }
2865
+ }
2866
+ return op;
2867
+ }
2868
+ };
2869
+
2870
+ // src/interceptor/RateLimitInterceptor.ts
2871
+ var RateLimitInterceptor = class {
2872
+ constructor(config = { windowMs: 1e3, maxOps: 50 }) {
2873
+ this.name = "RateLimitInterceptor";
2874
+ this.limits = /* @__PURE__ */ new Map();
2875
+ this.config = config;
2876
+ }
2877
+ async onBeforeOp(op, context) {
2878
+ const clientId = context.clientId;
2879
+ const now = Date.now();
2880
+ let limit = this.limits.get(clientId);
2881
+ if (!limit || now > limit.resetTime) {
2882
+ limit = {
2883
+ count: 0,
2884
+ resetTime: now + this.config.windowMs
2885
+ };
2886
+ this.limits.set(clientId, limit);
2887
+ }
2888
+ limit.count++;
2889
+ if (limit.count > this.config.maxOps) {
2890
+ logger.warn({ clientId, opId: op.id, count: limit.count }, "Rate limit exceeded");
2891
+ throw new Error("Rate limit exceeded");
2892
+ }
2893
+ return op;
2894
+ }
2895
+ // Cleanup old entries periodically?
2896
+ // For now we rely on resetTime check, but map grows.
2897
+ // Simple cleanup on reset logic:
2898
+ // In a real system, we'd use Redis or a proper cache with TTL.
2899
+ // Here we can just prune occasionally or relying on connection disconnect?
2900
+ // Optimization: Cleanup on disconnect
2901
+ async onDisconnect(context) {
2902
+ this.limits.delete(context.clientId);
2903
+ }
2904
+ };
2905
+ // Annotate the CommonJS export names for ESM import in node:
2906
+ 0 && (module.exports = {
2907
+ MemoryServerAdapter,
2908
+ PostgresAdapter,
2909
+ RateLimitInterceptor,
2910
+ SecurityManager,
2911
+ ServerCoordinator,
2912
+ TimestampInterceptor,
2913
+ logger
2914
+ });
2915
+ //# sourceMappingURL=index.js.map