redux-cluster 1.10.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +345 -471
  3. package/dist/cjs/core/backup.d.ts +10 -0
  4. package/dist/cjs/core/backup.d.ts.map +1 -0
  5. package/dist/cjs/core/backup.js +166 -0
  6. package/dist/cjs/core/redux-cluster.d.ts +47 -0
  7. package/dist/cjs/core/redux-cluster.d.ts.map +1 -0
  8. package/dist/cjs/core/redux-cluster.js +370 -0
  9. package/dist/cjs/index.d.ts +22 -0
  10. package/dist/cjs/index.d.ts.map +1 -0
  11. package/dist/cjs/index.js +43 -0
  12. package/dist/cjs/network/client.d.ts +23 -0
  13. package/dist/cjs/network/client.d.ts.map +1 -0
  14. package/dist/cjs/network/client.js +251 -0
  15. package/dist/cjs/network/server.d.ts +39 -0
  16. package/dist/cjs/network/server.d.ts.map +1 -0
  17. package/dist/cjs/network/server.js +440 -0
  18. package/dist/cjs/package.json +1 -0
  19. package/dist/cjs/types/index.d.ts +125 -0
  20. package/dist/cjs/types/index.d.ts.map +1 -0
  21. package/dist/cjs/types/index.js +20 -0
  22. package/dist/cjs/utils/crypto.d.ts +22 -0
  23. package/dist/cjs/utils/crypto.d.ts.map +1 -0
  24. package/dist/cjs/utils/crypto.js +404 -0
  25. package/dist/esm/core/backup.d.ts +10 -0
  26. package/dist/esm/core/backup.d.ts.map +1 -0
  27. package/dist/esm/core/backup.js +129 -0
  28. package/dist/esm/core/redux-cluster.d.ts +47 -0
  29. package/dist/esm/core/redux-cluster.d.ts.map +1 -0
  30. package/dist/esm/core/redux-cluster.js +363 -0
  31. package/dist/esm/index.d.ts +22 -0
  32. package/dist/esm/index.d.ts.map +1 -0
  33. package/dist/esm/index.js +25 -0
  34. package/dist/esm/network/client.d.ts +23 -0
  35. package/dist/esm/network/client.d.ts.map +1 -0
  36. package/dist/esm/network/client.js +214 -0
  37. package/dist/esm/network/server.d.ts +39 -0
  38. package/dist/esm/network/server.d.ts.map +1 -0
  39. package/dist/esm/network/server.js +403 -0
  40. package/dist/esm/package.json +1 -0
  41. package/dist/esm/types/index.d.ts +125 -0
  42. package/dist/esm/types/index.d.ts.map +1 -0
  43. package/dist/esm/types/index.js +17 -0
  44. package/dist/esm/utils/crypto.d.ts +22 -0
  45. package/dist/esm/utils/crypto.d.ts.map +1 -0
  46. package/dist/esm/utils/crypto.js +351 -0
  47. package/package.json +115 -34
  48. package/index.js +0 -678
  49. package/test.auto.js +0 -94
  50. package/test.auto.proc1.js +0 -97
  51. package/test.auto.proc2.js +0 -85
  52. package/test.visual.client.highload.js +0 -102
  53. package/test.visual.client.js +0 -103
  54. package/test.visual.error.js +0 -45
  55. package/test.visual.js +0 -97
  56. package/test.visual.server.highload.js +0 -102
  57. package/test.visual.server.js +0 -103
@@ -0,0 +1,363 @@
1
+ import { createStore } from "redux";
2
+ import cluster from "cluster";
3
+ import { readFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { hasher, universalClone, universalSerialize, universalDeserialize, createClassRegistry, } from "../utils/crypto";
6
+ import { MessageType, SerializationMode, } from "../types";
7
+ import { ClusterServer } from "../network/server";
8
+ import { ClusterClient } from "../network/client";
9
+ import { BackupManager } from "./backup";
10
+ // Global reducers registry to prevent name conflicts
11
+ const reducers = {};
12
+ export class ReduxCluster {
13
+ constructor(reducer, config = {}) {
14
+ this.role = [];
15
+ this.connected = false;
16
+ this.mode = "action";
17
+ this.resync = 1000;
18
+ this.stderr = console.error;
19
+ this.allsock = {};
20
+ this.classRegistry = createClassRegistry();
21
+ this.altReducer = reducer;
22
+ this.RCHash = hasher(reducer.name) || "";
23
+ // Set configuration with defaults
24
+ this.config = {
25
+ serializationMode: SerializationMode.JSON,
26
+ debug: false,
27
+ ...config,
28
+ };
29
+ // Apply configuration to instance properties
30
+ if (config.mode)
31
+ this.mode = config.mode;
32
+ if (config.role)
33
+ this.role.push(...config.role);
34
+ if (config.stderr)
35
+ this.stderr = config.stderr;
36
+ if (config.resync)
37
+ this.resync = config.resync;
38
+ // Initialize class registry only if ProtoObject mode is enabled
39
+ if (this.config.serializationMode === SerializationMode.PROTOOBJECT) {
40
+ this.classRegistry = createClassRegistry();
41
+ }
42
+ // Load package info
43
+ try {
44
+ let packagePath;
45
+ try {
46
+ // Try CommonJS approach first (__dirname available)
47
+ packagePath = join(__dirname, "../../package.json");
48
+ }
49
+ catch {
50
+ // Fallback - use relative path from process.cwd()
51
+ packagePath = join(process.cwd(), "package.json");
52
+ }
53
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
54
+ this.version = packageJson.version;
55
+ this.homepage = packageJson.homepage;
56
+ }
57
+ catch {
58
+ this.version = "2.0.0";
59
+ this.homepage = "https://github.com/siarheidudko/redux-cluster";
60
+ }
61
+ // Validate reducer uniqueness (only if different hash)
62
+ if (typeof reducers[reducer.name] !== "undefined" &&
63
+ reducers[reducer.name] !== this.RCHash) {
64
+ throw new Error("Please don't use a reducer with the same name!");
65
+ }
66
+ reducers[reducer.name] = this.RCHash;
67
+ // Get default state
68
+ try {
69
+ const defaultState = this.altReducer(undefined, {});
70
+ if (typeof defaultState === "object" && defaultState !== null) {
71
+ this.defaultState = defaultState;
72
+ }
73
+ else {
74
+ throw new Error("The returned value is not an object.");
75
+ }
76
+ }
77
+ catch {
78
+ this.defaultState = {};
79
+ }
80
+ // Create Redux store with custom reducer
81
+ this.store = createStore(this.createNewReducer());
82
+ // Bind Redux methods
83
+ this.dispatch = this.store.dispatch;
84
+ this.getState = this.store.getState;
85
+ this.subscribe = this.store.subscribe;
86
+ this.replaceReducer = this.store.replaceReducer;
87
+ this[Symbol.observable] = this.store[Symbol.observable];
88
+ this.initializeClusterRole();
89
+ }
90
+ // Internal method for sync actions
91
+ internalSync(payload) {
92
+ this.store.dispatch({
93
+ type: MessageType.SYNC,
94
+ payload,
95
+ _internal: true,
96
+ });
97
+ }
98
+ // Expose internal sync for backup purposes
99
+ _internalSync(payload) {
100
+ this.internalSync(payload);
101
+ }
102
+ createNewReducer() {
103
+ return (state = this.defaultState, action) => {
104
+ // Handle sync action (internal use only)
105
+ if (action.type === MessageType.SYNC) {
106
+ // Check if this is an internal sync call
107
+ const syncAction = action;
108
+ if (syncAction._internal) {
109
+ return universalClone(syncAction.payload, this.config.serializationMode, this.classRegistry);
110
+ }
111
+ else {
112
+ throw new Error("Please don't use REDUX_CLUSTER_SYNC action type!");
113
+ }
114
+ }
115
+ // Handle sync mode
116
+ if (this.mode === "action") {
117
+ this.updateCounter();
118
+ this.sendActionsToNodes(action);
119
+ }
120
+ return this.altReducer(state, action);
121
+ };
122
+ }
123
+ updateCounter() {
124
+ if (typeof this.counter === "undefined" || this.counter === this.resync) {
125
+ this.counter = 1;
126
+ }
127
+ else {
128
+ this.counter++;
129
+ }
130
+ // Periodic full sync
131
+ if (this.counter === this.resync) {
132
+ if (this.role.includes("master")) {
133
+ setTimeout(() => this.sendtoall(), 100);
134
+ }
135
+ if (this.role.includes("server")) {
136
+ setTimeout(() => this.sendtoallsock(), 100);
137
+ }
138
+ }
139
+ }
140
+ sendActionsToNodes(action) {
141
+ if (this.role.includes("master")) {
142
+ setTimeout(() => this.sendtoall({
143
+ _msg: MessageType.MSG_TO_WORKER,
144
+ _hash: this.RCHash,
145
+ _action: action,
146
+ }), 1);
147
+ }
148
+ if (this.role.includes("server")) {
149
+ setTimeout(() => this.sendtoallsock({
150
+ _msg: MessageType.MSG_TO_WORKER,
151
+ _hash: this.RCHash,
152
+ _action: action,
153
+ }), 1);
154
+ }
155
+ }
156
+ initializeClusterRole() {
157
+ // Assign "master" role only to primary process in cluster
158
+ if (cluster.isPrimary) {
159
+ this.initializeMaster();
160
+ }
161
+ else {
162
+ // Assign "worker" role to non-master processes
163
+ this.initializeWorker();
164
+ }
165
+ }
166
+ initializeMaster() {
167
+ if (!this.role.includes("master")) {
168
+ this.role.push("master");
169
+ }
170
+ // Subscribe to changes in snapshot mode
171
+ this.unsubscribe = this.subscribe(() => {
172
+ if (this.mode === "snapshot") {
173
+ this.sendtoall();
174
+ }
175
+ });
176
+ // Listen for messages from workers
177
+ cluster.on("message", (worker, message, _handle) => {
178
+ if (arguments.length === 2) {
179
+ _handle = message;
180
+ message = worker;
181
+ worker = undefined;
182
+ }
183
+ this.handleMasterMessage(message, worker);
184
+ });
185
+ this.connected = true;
186
+ }
187
+ initializeWorker() {
188
+ if (!this.role.includes("worker")) {
189
+ this.role.push("worker");
190
+ }
191
+ // Override dispatch to send to master
192
+ this.dispatchNEW = this.dispatch;
193
+ this.dispatch = (action) => {
194
+ if (process.send) {
195
+ process.send({
196
+ _msg: MessageType.MSG_TO_MASTER,
197
+ _hash: this.RCHash,
198
+ _action: action,
199
+ });
200
+ }
201
+ };
202
+ // Listen for messages from master
203
+ process.on("message", (data) => {
204
+ this.handleWorkerMessage(data);
205
+ });
206
+ this.connected = true;
207
+ // Request initial state
208
+ if (process.send) {
209
+ process.send({
210
+ _msg: MessageType.START,
211
+ _hash: this.RCHash,
212
+ });
213
+ }
214
+ }
215
+ handleMasterMessage(message, worker) {
216
+ if (message._hash === this.RCHash) {
217
+ switch (message._msg) {
218
+ case MessageType.MSG_TO_MASTER:
219
+ if (message._action.type === MessageType.SYNC &&
220
+ !message._action._internal) {
221
+ throw new Error("Please don't use REDUX_CLUSTER_SYNC action type!");
222
+ }
223
+ // Deserialize action if it contains ProtoObject
224
+ let action = message._action;
225
+ if (message._serialized) {
226
+ try {
227
+ action = universalDeserialize(message._serialized, this.config.serializationMode, this.classRegistry)._action;
228
+ }
229
+ catch {
230
+ // Fallback to regular action
231
+ action = message._action;
232
+ }
233
+ }
234
+ this.store.dispatch(action);
235
+ break;
236
+ case MessageType.START:
237
+ const responseMsg = {
238
+ _msg: MessageType.MSG_TO_WORKER,
239
+ _hash: this.RCHash,
240
+ _action: {
241
+ type: MessageType.SYNC,
242
+ payload: this.getState(),
243
+ _internal: true,
244
+ },
245
+ };
246
+ const serializedResponse = {
247
+ ...responseMsg,
248
+ _serialized: universalSerialize(responseMsg, this.config.serializationMode, this.classRegistry),
249
+ };
250
+ if (worker && cluster.workers && cluster.workers[worker.id]) {
251
+ cluster.workers[worker.id].send(serializedResponse);
252
+ }
253
+ else {
254
+ this.sendtoall();
255
+ }
256
+ break;
257
+ default:
258
+ // Ignore unknown message types
259
+ break;
260
+ }
261
+ }
262
+ }
263
+ handleWorkerMessage(data) {
264
+ if (data._hash === this.RCHash && this.role.includes("worker")) {
265
+ // Deserialize ProtoObject if needed
266
+ let processedData = data;
267
+ if (data._serialized) {
268
+ try {
269
+ processedData = JSON.parse(data._serialized);
270
+ processedData = universalDeserialize(data._serialized, this.config.serializationMode, this.classRegistry);
271
+ }
272
+ catch {
273
+ // Fallback to regular data if deserialization fails
274
+ processedData = data;
275
+ }
276
+ }
277
+ switch (processedData._msg) {
278
+ case MessageType.MSG_TO_WORKER:
279
+ if (this.dispatchNEW) {
280
+ this.dispatchNEW(processedData._action);
281
+ }
282
+ break;
283
+ case MessageType.CONN_STATUS:
284
+ this.connected = processedData._connected;
285
+ break;
286
+ default:
287
+ // Ignore unknown message types
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ sendtoall(message) {
293
+ if (cluster.isPrimary && cluster.workers) {
294
+ const msg = message || {
295
+ _msg: MessageType.MSG_TO_WORKER,
296
+ _hash: this.RCHash,
297
+ _action: {
298
+ type: MessageType.SYNC,
299
+ payload: this.getState(),
300
+ _internal: true,
301
+ },
302
+ };
303
+ // Serialize ProtoObject instances for IPC
304
+ const serializedMsg = {
305
+ ...msg,
306
+ _serialized: universalSerialize(msg, this.config.serializationMode, this.classRegistry),
307
+ };
308
+ for (const id in cluster.workers) {
309
+ if (cluster.workers[id]) {
310
+ cluster.workers[id].send(serializedMsg);
311
+ }
312
+ }
313
+ }
314
+ }
315
+ sendtoallsock(message) {
316
+ for (const id in this.allsock) {
317
+ if (typeof this.allsock[id] === "object" &&
318
+ typeof this.allsock[id].sendtoall === "function") {
319
+ setTimeout(() => this.allsock[id].sendtoall(message), 1);
320
+ }
321
+ }
322
+ }
323
+ createServer(settings) {
324
+ if (!cluster.isPrimary && settings?.path && process.platform === "win32") {
325
+ throw new Error("Named channel is not supported in the child process, please use TCP-server");
326
+ }
327
+ if (!this.role.includes("server")) {
328
+ this.role.push("server");
329
+ }
330
+ this.connected = false;
331
+ return new ClusterServer(this, settings);
332
+ }
333
+ createClient(settings) {
334
+ if (this.role.includes("client")) {
335
+ throw new Error("One storage cannot be connected to two servers at the same time.");
336
+ }
337
+ if (!this.role.includes("client")) {
338
+ this.role.push("client");
339
+ }
340
+ this.connected = false;
341
+ return new ClusterClient(this, settings);
342
+ }
343
+ backup(settings) {
344
+ return new BackupManager(this, settings).initialize();
345
+ }
346
+ // Register custom ProtoObject classes for proper serialization
347
+ registerClass(name, classConstructor) {
348
+ if (this.config.serializationMode === SerializationMode.PROTOOBJECT) {
349
+ this.classRegistry.set(name, classConstructor);
350
+ }
351
+ else {
352
+ console.warn("registerClass() is only available in ProtoObject mode");
353
+ }
354
+ }
355
+ // Get registered classes (useful for debugging)
356
+ getRegisteredClasses() {
357
+ if (this.config.serializationMode === SerializationMode.PROTOOBJECT) {
358
+ return Array.from(this.classRegistry.keys());
359
+ }
360
+ return [];
361
+ }
362
+ }
363
+ Symbol.observable;
@@ -0,0 +1,22 @@
1
+ import { Reducer, Action, combineReducers, bindActionCreators, applyMiddleware, compose } from "redux";
2
+ import { ReduxCluster } from "./core/redux-cluster";
3
+ import { ReduxClusterConfig } from "./types";
4
+ import { hasher } from "./utils/crypto";
5
+ export * from "redux";
6
+ export * from "./types";
7
+ export declare const functions: {
8
+ hasher: typeof hasher;
9
+ };
10
+ export declare function createStore<S = any, A extends Action = Action>(reducer: Reducer<S, A>, config?: ReduxClusterConfig): ReduxCluster<S, A>;
11
+ declare const _default: {
12
+ createStore: typeof createStore;
13
+ functions: {
14
+ hasher: typeof hasher;
15
+ };
16
+ combineReducers: typeof combineReducers;
17
+ bindActionCreators: typeof bindActionCreators;
18
+ applyMiddleware: typeof applyMiddleware;
19
+ compose: typeof compose;
20
+ };
21
+ export default _default;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,MAAM,EACN,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,OAAO,EACR,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,cAAc,OAAO,CAAC;AAGtB,cAAc,SAAS,CAAC;AAGxB,eAAO,MAAM,SAAS;;CAErB,CAAC;AAGF,wBAAgB,WAAW,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,EAC5D,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,EACtB,MAAM,CAAC,EAAE,kBAAkB,GAC1B,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAEpB;;;;;;;;;;;AAGD,wBAQE"}
@@ -0,0 +1,25 @@
1
+ import { combineReducers, bindActionCreators, applyMiddleware, compose, } from "redux";
2
+ import { ReduxCluster } from "./core/redux-cluster";
3
+ import { hasher } from "./utils/crypto";
4
+ // Re-export Redux functions and types
5
+ export * from "redux";
6
+ // Export our types
7
+ export * from "./types";
8
+ // Export utility functions
9
+ export const functions = {
10
+ hasher,
11
+ };
12
+ // Main function to create a ReduxCluster store
13
+ export function createStore(reducer, config) {
14
+ return new ReduxCluster(reducer, config);
15
+ }
16
+ // Default export for CommonJS compatibility
17
+ export default {
18
+ createStore,
19
+ functions,
20
+ // Re-export Redux
21
+ combineReducers,
22
+ bindActionCreators,
23
+ applyMiddleware,
24
+ compose,
25
+ };
@@ -0,0 +1,23 @@
1
+ import { ReduxClusterStore, ClientSettings } from "../types";
2
+ export declare class ClusterClient {
3
+ private store;
4
+ private settings;
5
+ login?: string;
6
+ password?: string;
7
+ private client;
8
+ private listenOptions;
9
+ private shouldReconnect;
10
+ private reconnectTimeout?;
11
+ constructor(store: ReduxClusterStore, settings?: ClientSettings);
12
+ private setupCredentials;
13
+ private setupConnection;
14
+ private getListenOptions;
15
+ private setupEventHandlers;
16
+ private handleConnection;
17
+ private handleDisconnection;
18
+ private setupPipeline;
19
+ private handleServerMessage;
20
+ private connectToServer;
21
+ disconnect(): void;
22
+ }
23
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/network/client.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAe,MAAM,UAAU,CAAC;AAG1E,qBAAa,aAAa;IAUtB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ;IAVX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAEzB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,gBAAgB,CAAC,CAAiB;gBAGhC,KAAK,EAAE,iBAAiB,EACxB,QAAQ,GAAE,cAAmB;IAOvC,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;IAuBxB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,gBAAgB;IA6CxB,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,aAAa;IA2DrB,OAAO,CAAC,mBAAmB;IA4C3B,OAAO,CAAC,eAAe;IAIhB,UAAU,IAAI,IAAI;CAmB1B"}
@@ -0,0 +1,214 @@
1
+ import * as net from "net";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import * as zlib from "zlib";
5
+ import * as stream from "stream";
6
+ import { MessageType } from "../types";
7
+ import { hasher } from "../utils/crypto";
8
+ export class ClusterClient {
9
+ constructor(store, settings = {}) {
10
+ this.store = store;
11
+ this.settings = settings;
12
+ this.shouldReconnect = true;
13
+ this.setupCredentials();
14
+ this.setupConnection();
15
+ this.connectToServer();
16
+ }
17
+ setupCredentials() {
18
+ if (typeof this.settings.login === "string") {
19
+ this.login = hasher(`REDUX_CLUSTER${this.settings.login}`);
20
+ }
21
+ if (typeof this.settings.password === "string") {
22
+ this.password = hasher(`REDUX_CLUSTER${this.settings.password}`);
23
+ }
24
+ }
25
+ setupConnection() {
26
+ this.listenOptions = this.getListenOptions();
27
+ this.client = new net.Socket();
28
+ this.setupEventHandlers();
29
+ this.setupPipeline();
30
+ }
31
+ getListenOptions() {
32
+ const defaultOptions = { port: 10001 };
33
+ if (typeof this.settings.path === "string") {
34
+ switch (os.platform()) {
35
+ case "win32":
36
+ return { path: path.join("\\\\?\\pipe", this.settings.path) };
37
+ default:
38
+ return { path: path.join(this.settings.path) };
39
+ }
40
+ }
41
+ const options = { ...defaultOptions };
42
+ if (typeof this.settings.host === "string") {
43
+ options.host = this.settings.host;
44
+ }
45
+ if (typeof this.settings.port === "number") {
46
+ options.port = this.settings.port;
47
+ }
48
+ return options;
49
+ }
50
+ setupEventHandlers() {
51
+ this.client.on("connect", () => {
52
+ this.handleConnection();
53
+ });
54
+ this.client.on("close", () => {
55
+ this.handleDisconnection();
56
+ });
57
+ this.client.on("error", (err) => {
58
+ this.store.stderr(`ReduxCluster.createClient client error: ${err.message}`);
59
+ });
60
+ }
61
+ handleConnection() {
62
+ this.store.connected = true;
63
+ this.store.sendtoall({
64
+ _msg: MessageType.CONN_STATUS,
65
+ _hash: this.store.RCHash,
66
+ _connected: true,
67
+ });
68
+ // Override write method for object mode + compression
69
+ this.client.writeNEW = this.client.write;
70
+ this.client.write = (data) => {
71
+ try {
72
+ const compressed = zlib.gzipSync(Buffer.from(JSON.stringify(data)));
73
+ return this.client.writeNEW(compressed);
74
+ }
75
+ catch (err) {
76
+ this.store.stderr(`ReduxCluster.createClient write error: ${err.message}`);
77
+ return false;
78
+ }
79
+ };
80
+ // Save original dispatch for local state updates
81
+ if (typeof this.store.dispatchNEW !== "function") {
82
+ this.store.dispatchNEW = this.store.dispatch;
83
+ }
84
+ // Override dispatch to send actions to server
85
+ this.store.dispatch = (action) => {
86
+ this.client.write({
87
+ _msg: MessageType.MSG_TO_MASTER,
88
+ _hash: this.store.RCHash,
89
+ _action: action,
90
+ });
91
+ };
92
+ // Authenticate with server
93
+ this.client.write({
94
+ _msg: MessageType.SOCKET_AUTH,
95
+ _hash: this.store.RCHash,
96
+ _login: this.login,
97
+ _password: this.password,
98
+ });
99
+ }
100
+ handleDisconnection() {
101
+ this.store.connected = false;
102
+ this.store.sendtoall({
103
+ _msg: MessageType.CONN_STATUS,
104
+ _hash: this.store.RCHash,
105
+ _connected: false,
106
+ });
107
+ // Reconnect after 250ms only if reconnection is enabled
108
+ if (this.shouldReconnect) {
109
+ this.reconnectTimeout = setTimeout(() => {
110
+ new ClusterClient(this.store, this.settings);
111
+ }, 250);
112
+ }
113
+ }
114
+ setupPipeline() {
115
+ // Create processing pipeline
116
+ const mbstring = new stream.Transform({
117
+ transform(buffer, encoding, callback) {
118
+ this.push(buffer);
119
+ callback();
120
+ },
121
+ });
122
+ mbstring.setEncoding("utf8");
123
+ const gunzipper = zlib.createGunzip();
124
+ // Simple JSON parser
125
+ const parser = new stream.Transform({
126
+ objectMode: true,
127
+ transform(chunk, encoding, callback) {
128
+ try {
129
+ const data = JSON.parse(chunk.toString());
130
+ this.push(data);
131
+ callback();
132
+ }
133
+ catch {
134
+ callback(); // Invalid JSON, ignore but continue
135
+ }
136
+ },
137
+ });
138
+ const eventHandler = new stream.Writable({
139
+ objectMode: true,
140
+ write: (data, encoding, callback) => {
141
+ this.handleServerMessage(data);
142
+ callback();
143
+ },
144
+ });
145
+ // Setup error handlers
146
+ [gunzipper, mbstring, parser, eventHandler].forEach((streamObj) => {
147
+ streamObj.on("error", (err) => {
148
+ this.store.stderr(`ReduxCluster.createClient stream error: ${err.message}`);
149
+ });
150
+ });
151
+ // Connect pipeline
152
+ this.client.pipe(gunzipper).pipe(mbstring).pipe(parser).pipe(eventHandler);
153
+ }
154
+ handleServerMessage(data) {
155
+ if (this.client.destroyed || data._hash !== this.store.RCHash) {
156
+ return;
157
+ }
158
+ switch (data._msg) {
159
+ case MessageType.MSG_TO_WORKER:
160
+ // Always use dispatchNEW (original dispatch) for local state updates
161
+ // because the regular dispatch is overridden to send to server
162
+ if (this.store.dispatchNEW) {
163
+ // Mark SYNC actions as internal
164
+ let actionToDispatch = data._action;
165
+ if (actionToDispatch.type === "REDUX_CLUSTER_SYNC") {
166
+ actionToDispatch = { ...actionToDispatch, _internal: true };
167
+ }
168
+ this.store.dispatchNEW(actionToDispatch);
169
+ }
170
+ else {
171
+ this.store.stderr("dispatchNEW method not available");
172
+ }
173
+ break;
174
+ case MessageType.SOCKET_AUTH_STATE:
175
+ if (data._value === true) {
176
+ // Authentication successful, request initial state
177
+ this.client.write({
178
+ _msg: MessageType.START,
179
+ _hash: this.store.RCHash,
180
+ });
181
+ }
182
+ else {
183
+ // Authentication failed
184
+ const error = data._banned
185
+ ? new Error("your ip is locked for 3 hours")
186
+ : new Error("authorization failed");
187
+ this.client.destroy(error);
188
+ }
189
+ break;
190
+ default:
191
+ // Ignore unknown message types
192
+ break;
193
+ }
194
+ }
195
+ connectToServer() {
196
+ this.client.connect(this.listenOptions);
197
+ }
198
+ disconnect() {
199
+ this.shouldReconnect = false;
200
+ // Clear reconnection timeout if it exists
201
+ if (this.reconnectTimeout) {
202
+ clearTimeout(this.reconnectTimeout);
203
+ this.reconnectTimeout = undefined;
204
+ }
205
+ // Remove all event listeners
206
+ if (this.client) {
207
+ this.client.removeAllListeners();
208
+ // Destroy the socket
209
+ if (!this.client.destroyed) {
210
+ this.client.destroy();
211
+ }
212
+ }
213
+ }
214
+ }
@@ -0,0 +1,39 @@
1
+ import { ServerSettings, ReduxClusterStore, ClusterMessage, ClusterSocket } from "../types";
2
+ export declare class ClusterServer {
3
+ private store;
4
+ private settings;
5
+ readonly uid: string;
6
+ readonly sockets: Record<string, ClusterSocket>;
7
+ readonly database: Record<string, string>;
8
+ readonly ip2ban: Record<string, {
9
+ time: number;
10
+ count: number;
11
+ }>;
12
+ private readonly ip2banTimeout;
13
+ private readonly ip2banGC;
14
+ private server;
15
+ private unsubscribe?;
16
+ private shouldAutoRestart;
17
+ constructor(store: ReduxClusterStore, settings?: ServerSettings);
18
+ private setupDatabase;
19
+ private setupBanSystem;
20
+ private cleanupBannedIPs;
21
+ private setupServer;
22
+ private getListenOptions;
23
+ private startListening;
24
+ private handleNewConnection;
25
+ private isIPBanned;
26
+ private rejectConnection;
27
+ private setupSocket;
28
+ private setupSocketPipeline;
29
+ private handleSocketMessage;
30
+ private handleAuthentication;
31
+ private recordFailedLogin;
32
+ private closeSocket;
33
+ private setupStoreIntegration;
34
+ sendtoall(message?: ClusterMessage): void;
35
+ ip2banGCStop(): void;
36
+ private cleanup;
37
+ close(): Promise<void>;
38
+ }
39
+ //# sourceMappingURL=server.d.ts.map