redux-cluster 1.10.0 → 2.0.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.
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 +367 -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 +439 -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 +134 -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 +376 -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 +221 -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 +408 -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,376 @@
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
+ // Redux Store properties
14
+ dispatch;
15
+ getState;
16
+ subscribe;
17
+ replaceReducer;
18
+ [Symbol.observable];
19
+ // ReduxCluster specific properties
20
+ RCHash;
21
+ version;
22
+ homepage;
23
+ role = [];
24
+ connected = false;
25
+ mode = "action";
26
+ resync = 1000;
27
+ stderr = console.error;
28
+ config;
29
+ altReducer;
30
+ defaultState;
31
+ store;
32
+ allsock = {};
33
+ counter;
34
+ dispatchNEW;
35
+ unsubscribe;
36
+ classRegistry = createClassRegistry();
37
+ constructor(reducer, config = {}) {
38
+ this.altReducer = reducer;
39
+ this.RCHash = hasher(reducer.name) || "";
40
+ // Set configuration with defaults
41
+ this.config = {
42
+ serializationMode: SerializationMode.JSON,
43
+ debug: false,
44
+ ...config,
45
+ };
46
+ // Apply configuration to instance properties
47
+ if (config.mode)
48
+ this.mode = config.mode;
49
+ if (config.role)
50
+ this.role.push(...config.role);
51
+ if (config.stderr)
52
+ this.stderr = config.stderr;
53
+ if (config.resync)
54
+ this.resync = config.resync;
55
+ // Initialize class registry only if ProtoObject mode is enabled
56
+ if (this.config.serializationMode === SerializationMode.PROTOOBJECT) {
57
+ this.classRegistry = createClassRegistry();
58
+ }
59
+ // Load package info
60
+ try {
61
+ let packagePath;
62
+ try {
63
+ // Try CommonJS approach first (__dirname available)
64
+ packagePath = join(__dirname, "../../package.json");
65
+ }
66
+ catch {
67
+ // Fallback - use relative path from process.cwd()
68
+ packagePath = join(process.cwd(), "package.json");
69
+ }
70
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
71
+ this.version = packageJson.version;
72
+ this.homepage = packageJson.homepage;
73
+ }
74
+ catch {
75
+ this.version = "2.0.0";
76
+ this.homepage = "https://github.com/siarheidudko/redux-cluster";
77
+ }
78
+ // Validate reducer uniqueness (only if different hash)
79
+ if (typeof reducers[reducer.name] !== "undefined" &&
80
+ reducers[reducer.name] !== this.RCHash) {
81
+ throw new Error("Please don't use a reducer with the same name!");
82
+ }
83
+ reducers[reducer.name] = this.RCHash;
84
+ // Get default state
85
+ try {
86
+ const defaultState = this.altReducer(undefined, {});
87
+ if (typeof defaultState === "object" && defaultState !== null) {
88
+ this.defaultState = defaultState;
89
+ }
90
+ else {
91
+ throw new Error("The returned value is not an object.");
92
+ }
93
+ }
94
+ catch {
95
+ this.defaultState = {};
96
+ }
97
+ // Create Redux store with custom reducer
98
+ this.store = createStore(this.createNewReducer());
99
+ // Bind Redux methods
100
+ this.dispatch = this.store.dispatch;
101
+ this.getState = this.store.getState;
102
+ this.subscribe = this.store.subscribe;
103
+ this.replaceReducer = this.store.replaceReducer;
104
+ this[Symbol.observable] = this.store[Symbol.observable];
105
+ this.initializeClusterRole();
106
+ }
107
+ // Internal method for sync actions
108
+ internalSync(payload) {
109
+ this.store.dispatch({
110
+ type: MessageType.SYNC,
111
+ payload,
112
+ _internal: true,
113
+ });
114
+ }
115
+ // Expose internal sync for backup purposes
116
+ _internalSync(payload) {
117
+ this.internalSync(payload);
118
+ }
119
+ createNewReducer() {
120
+ return (state = this.defaultState, action) => {
121
+ // Handle sync action (internal use only)
122
+ if (action.type === MessageType.SYNC) {
123
+ // Check if this is an internal sync call
124
+ const syncAction = action;
125
+ if (syncAction._internal) {
126
+ return universalClone(syncAction.payload, this.config.serializationMode, this.classRegistry);
127
+ }
128
+ else {
129
+ throw new Error("Please don't use REDUX_CLUSTER_SYNC action type!");
130
+ }
131
+ }
132
+ // Handle sync mode
133
+ if (this.mode === "action") {
134
+ this.updateCounter();
135
+ this.sendActionsToNodes(action);
136
+ }
137
+ return this.altReducer(state, action);
138
+ };
139
+ }
140
+ updateCounter() {
141
+ if (typeof this.counter === "undefined" || this.counter === this.resync) {
142
+ this.counter = 1;
143
+ }
144
+ else {
145
+ this.counter++;
146
+ }
147
+ // Periodic full sync
148
+ if (this.counter === this.resync) {
149
+ if (this.role.includes("master")) {
150
+ setTimeout(() => this.sendtoall(), 100);
151
+ }
152
+ if (this.role.includes("server")) {
153
+ setTimeout(() => this.sendtoallsock(), 100);
154
+ }
155
+ }
156
+ }
157
+ sendActionsToNodes(action) {
158
+ if (this.role.includes("master")) {
159
+ setTimeout(() => this.sendtoall({
160
+ _msg: MessageType.MSG_TO_WORKER,
161
+ _hash: this.RCHash,
162
+ _action: action,
163
+ }), 1);
164
+ }
165
+ if (this.role.includes("server")) {
166
+ setTimeout(() => this.sendtoallsock({
167
+ _msg: MessageType.MSG_TO_WORKER,
168
+ _hash: this.RCHash,
169
+ _action: action,
170
+ }), 1);
171
+ }
172
+ }
173
+ initializeClusterRole() {
174
+ // Assign "master" role only to primary process in cluster
175
+ if (cluster.isPrimary) {
176
+ this.initializeMaster();
177
+ }
178
+ else {
179
+ // Assign "worker" role to non-master processes
180
+ this.initializeWorker();
181
+ }
182
+ }
183
+ initializeMaster() {
184
+ if (!this.role.includes("master")) {
185
+ this.role.push("master");
186
+ }
187
+ // Subscribe to changes in snapshot mode
188
+ this.unsubscribe = this.subscribe(() => {
189
+ if (this.mode === "snapshot") {
190
+ this.sendtoall();
191
+ }
192
+ });
193
+ // Listen for messages from workers
194
+ cluster.on("message", (worker, message, _handle) => {
195
+ if (arguments.length === 2) {
196
+ _handle = message;
197
+ message = worker;
198
+ worker = undefined;
199
+ }
200
+ this.handleMasterMessage(message, worker);
201
+ });
202
+ this.connected = true;
203
+ }
204
+ initializeWorker() {
205
+ if (!this.role.includes("worker")) {
206
+ this.role.push("worker");
207
+ }
208
+ // Override dispatch to send to master
209
+ this.dispatchNEW = this.dispatch;
210
+ this.dispatch = (action) => {
211
+ if (process.send) {
212
+ process.send({
213
+ _msg: MessageType.MSG_TO_MASTER,
214
+ _hash: this.RCHash,
215
+ _action: action,
216
+ });
217
+ }
218
+ };
219
+ // Listen for messages from master
220
+ process.on("message", (data) => {
221
+ this.handleWorkerMessage(data);
222
+ });
223
+ this.connected = true;
224
+ // Request initial state
225
+ if (process.send) {
226
+ process.send({
227
+ _msg: MessageType.START,
228
+ _hash: this.RCHash,
229
+ });
230
+ }
231
+ }
232
+ handleMasterMessage(message, worker) {
233
+ if (message._hash === this.RCHash) {
234
+ switch (message._msg) {
235
+ case MessageType.MSG_TO_MASTER:
236
+ if (message._action.type === MessageType.SYNC) {
237
+ throw new Error("Please don't use REDUX_CLUSTER_SYNC action type!");
238
+ }
239
+ // Deserialize action if it contains ProtoObject
240
+ let action = message._action;
241
+ if (message._serialized) {
242
+ try {
243
+ action = universalDeserialize(message._serialized, this.config.serializationMode, this.classRegistry)._action;
244
+ }
245
+ catch {
246
+ // Fallback to regular action
247
+ action = message._action;
248
+ }
249
+ }
250
+ this.store.dispatch(action);
251
+ break;
252
+ case MessageType.START:
253
+ const responseMsg = {
254
+ _msg: MessageType.MSG_TO_WORKER,
255
+ _hash: this.RCHash,
256
+ _action: {
257
+ type: MessageType.SYNC,
258
+ payload: this.getState(),
259
+ },
260
+ };
261
+ const serializedResponse = {
262
+ ...responseMsg,
263
+ _serialized: universalSerialize(responseMsg, this.config.serializationMode, this.classRegistry),
264
+ };
265
+ if (worker && cluster.workers && cluster.workers[worker.id]) {
266
+ cluster.workers[worker.id].send(serializedResponse);
267
+ }
268
+ else {
269
+ this.sendtoall();
270
+ }
271
+ break;
272
+ default:
273
+ // Ignore unknown message types
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ handleWorkerMessage(data) {
279
+ if (data._hash === this.RCHash && this.role.includes("worker")) {
280
+ // Deserialize ProtoObject if needed
281
+ let processedData = data;
282
+ if (data._serialized) {
283
+ try {
284
+ processedData = JSON.parse(data._serialized);
285
+ processedData = universalDeserialize(data._serialized, this.config.serializationMode, this.classRegistry);
286
+ }
287
+ catch {
288
+ // Fallback to regular data if deserialization fails
289
+ processedData = data;
290
+ }
291
+ }
292
+ switch (processedData._msg) {
293
+ case MessageType.MSG_TO_WORKER:
294
+ if (this.dispatchNEW) {
295
+ this.dispatchNEW(processedData._action);
296
+ }
297
+ break;
298
+ case MessageType.CONN_STATUS:
299
+ this.connected = processedData._connected;
300
+ break;
301
+ default:
302
+ // Ignore unknown message types
303
+ break;
304
+ }
305
+ }
306
+ }
307
+ sendtoall(message) {
308
+ if (cluster.isPrimary && cluster.workers) {
309
+ const msg = message || {
310
+ _msg: MessageType.MSG_TO_WORKER,
311
+ _hash: this.RCHash,
312
+ _action: {
313
+ type: MessageType.SYNC,
314
+ payload: this.getState(),
315
+ },
316
+ };
317
+ // Serialize ProtoObject instances for IPC
318
+ const serializedMsg = {
319
+ ...msg,
320
+ _serialized: universalSerialize(msg, this.config.serializationMode, this.classRegistry),
321
+ };
322
+ for (const id in cluster.workers) {
323
+ if (cluster.workers[id]) {
324
+ cluster.workers[id].send(serializedMsg);
325
+ }
326
+ }
327
+ }
328
+ }
329
+ sendtoallsock(message) {
330
+ for (const id in this.allsock) {
331
+ if (typeof this.allsock[id] === "object" &&
332
+ typeof this.allsock[id].sendtoall === "function") {
333
+ setTimeout(() => this.allsock[id].sendtoall(message), 1);
334
+ }
335
+ }
336
+ }
337
+ createServer(settings) {
338
+ if (!cluster.isPrimary && settings?.path && process.platform === "win32") {
339
+ throw new Error("Named channel is not supported in the child process, please use TCP-server");
340
+ }
341
+ if (!this.role.includes("server")) {
342
+ this.role.push("server");
343
+ }
344
+ this.connected = false;
345
+ return new ClusterServer(this, settings);
346
+ }
347
+ createClient(settings) {
348
+ if (this.role.includes("client")) {
349
+ throw new Error("One storage cannot be connected to two servers at the same time.");
350
+ }
351
+ if (!this.role.includes("client")) {
352
+ this.role.push("client");
353
+ }
354
+ this.connected = false;
355
+ return new ClusterClient(this, settings);
356
+ }
357
+ backup(settings) {
358
+ return new BackupManager(this, settings).initialize();
359
+ }
360
+ // Register custom ProtoObject classes for proper serialization
361
+ registerClass(name, classConstructor) {
362
+ if (this.config.serializationMode === SerializationMode.PROTOOBJECT) {
363
+ this.classRegistry.set(name, classConstructor);
364
+ }
365
+ else {
366
+ console.warn("registerClass() is only available in ProtoObject mode");
367
+ }
368
+ }
369
+ // Get registered classes (useful for debugging)
370
+ getRegisteredClasses() {
371
+ if (this.config.serializationMode === SerializationMode.PROTOOBJECT) {
372
+ return Array.from(this.classRegistry.keys());
373
+ }
374
+ return [];
375
+ }
376
+ }
@@ -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,221 @@
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
+ store;
10
+ settings;
11
+ login;
12
+ password;
13
+ client;
14
+ listenOptions;
15
+ shouldReconnect = true;
16
+ reconnectTimeout;
17
+ constructor(store, settings = {}) {
18
+ this.store = store;
19
+ this.settings = settings;
20
+ this.setupCredentials();
21
+ this.setupConnection();
22
+ this.connectToServer();
23
+ }
24
+ setupCredentials() {
25
+ if (typeof this.settings.login === "string") {
26
+ this.login = hasher(`REDUX_CLUSTER${this.settings.login}`);
27
+ }
28
+ if (typeof this.settings.password === "string") {
29
+ this.password = hasher(`REDUX_CLUSTER${this.settings.password}`);
30
+ }
31
+ }
32
+ setupConnection() {
33
+ this.listenOptions = this.getListenOptions();
34
+ this.client = new net.Socket();
35
+ this.setupEventHandlers();
36
+ this.setupPipeline();
37
+ }
38
+ getListenOptions() {
39
+ const defaultOptions = { port: 10001 };
40
+ if (typeof this.settings.path === "string") {
41
+ switch (os.platform()) {
42
+ case "win32":
43
+ return { path: path.join("\\\\?\\pipe", this.settings.path) };
44
+ default:
45
+ return { path: path.join(this.settings.path) };
46
+ }
47
+ }
48
+ const options = { ...defaultOptions };
49
+ if (typeof this.settings.host === "string") {
50
+ options.host = this.settings.host;
51
+ }
52
+ if (typeof this.settings.port === "number") {
53
+ options.port = this.settings.port;
54
+ }
55
+ return options;
56
+ }
57
+ setupEventHandlers() {
58
+ this.client.on("connect", () => {
59
+ this.handleConnection();
60
+ });
61
+ this.client.on("close", () => {
62
+ this.handleDisconnection();
63
+ });
64
+ this.client.on("error", (err) => {
65
+ this.store.stderr(`ReduxCluster.createClient client error: ${err.message}`);
66
+ });
67
+ }
68
+ handleConnection() {
69
+ this.store.connected = true;
70
+ this.store.sendtoall({
71
+ _msg: MessageType.CONN_STATUS,
72
+ _hash: this.store.RCHash,
73
+ _connected: true,
74
+ });
75
+ // Override write method for object mode + compression
76
+ this.client.writeNEW = this.client.write;
77
+ this.client.write = (data) => {
78
+ try {
79
+ const compressed = zlib.gzipSync(Buffer.from(JSON.stringify(data)));
80
+ return this.client.writeNEW(compressed);
81
+ }
82
+ catch (err) {
83
+ this.store.stderr(`ReduxCluster.createClient write error: ${err.message}`);
84
+ return false;
85
+ }
86
+ };
87
+ // Save original dispatch for local state updates
88
+ if (typeof this.store.dispatchNEW !== "function") {
89
+ this.store.dispatchNEW = this.store.dispatch;
90
+ }
91
+ // Override dispatch to send actions to server
92
+ this.store.dispatch = (action) => {
93
+ this.client.write({
94
+ _msg: MessageType.MSG_TO_MASTER,
95
+ _hash: this.store.RCHash,
96
+ _action: action,
97
+ });
98
+ };
99
+ // Authenticate with server
100
+ this.client.write({
101
+ _msg: MessageType.SOCKET_AUTH,
102
+ _hash: this.store.RCHash,
103
+ _login: this.login,
104
+ _password: this.password,
105
+ });
106
+ }
107
+ handleDisconnection() {
108
+ this.store.connected = false;
109
+ this.store.sendtoall({
110
+ _msg: MessageType.CONN_STATUS,
111
+ _hash: this.store.RCHash,
112
+ _connected: false,
113
+ });
114
+ // Reconnect after 250ms only if reconnection is enabled
115
+ if (this.shouldReconnect) {
116
+ this.reconnectTimeout = setTimeout(() => {
117
+ new ClusterClient(this.store, this.settings);
118
+ }, 250);
119
+ }
120
+ }
121
+ setupPipeline() {
122
+ // Create processing pipeline
123
+ const mbstring = new stream.Transform({
124
+ transform(buffer, encoding, callback) {
125
+ this.push(buffer);
126
+ callback();
127
+ },
128
+ });
129
+ mbstring.setEncoding("utf8");
130
+ const gunzipper = zlib.createGunzip();
131
+ // Simple JSON parser
132
+ const parser = new stream.Transform({
133
+ objectMode: true,
134
+ transform(chunk, encoding, callback) {
135
+ try {
136
+ const data = JSON.parse(chunk.toString());
137
+ this.push(data);
138
+ callback();
139
+ }
140
+ catch {
141
+ callback(); // Invalid JSON, ignore but continue
142
+ }
143
+ },
144
+ });
145
+ const eventHandler = new stream.Writable({
146
+ objectMode: true,
147
+ write: (data, encoding, callback) => {
148
+ this.handleServerMessage(data);
149
+ callback();
150
+ },
151
+ });
152
+ // Setup error handlers
153
+ [gunzipper, mbstring, parser, eventHandler].forEach((streamObj) => {
154
+ streamObj.on("error", (err) => {
155
+ this.store.stderr(`ReduxCluster.createClient stream error: ${err.message}`);
156
+ });
157
+ });
158
+ // Connect pipeline
159
+ this.client.pipe(gunzipper).pipe(mbstring).pipe(parser).pipe(eventHandler);
160
+ }
161
+ handleServerMessage(data) {
162
+ if (this.client.destroyed || data._hash !== this.store.RCHash) {
163
+ return;
164
+ }
165
+ switch (data._msg) {
166
+ case MessageType.MSG_TO_WORKER:
167
+ // Always use dispatchNEW (original dispatch) for local state updates
168
+ // because the regular dispatch is overridden to send to server
169
+ if (this.store.dispatchNEW) {
170
+ // Mark SYNC actions as internal
171
+ let actionToDispatch = data._action;
172
+ if (actionToDispatch.type === "REDUX_CLUSTER_SYNC") {
173
+ actionToDispatch = { ...actionToDispatch, _internal: true };
174
+ }
175
+ this.store.dispatchNEW(actionToDispatch);
176
+ }
177
+ else {
178
+ this.store.stderr("dispatchNEW method not available");
179
+ }
180
+ break;
181
+ case MessageType.SOCKET_AUTH_STATE:
182
+ if (data._value === true) {
183
+ // Authentication successful, request initial state
184
+ this.client.write({
185
+ _msg: MessageType.START,
186
+ _hash: this.store.RCHash,
187
+ });
188
+ }
189
+ else {
190
+ // Authentication failed
191
+ const error = data._banned
192
+ ? new Error("your ip is locked for 3 hours")
193
+ : new Error("authorization failed");
194
+ this.client.destroy(error);
195
+ }
196
+ break;
197
+ default:
198
+ // Ignore unknown message types
199
+ break;
200
+ }
201
+ }
202
+ connectToServer() {
203
+ this.client.connect(this.listenOptions);
204
+ }
205
+ disconnect() {
206
+ this.shouldReconnect = false;
207
+ // Clear reconnection timeout if it exists
208
+ if (this.reconnectTimeout) {
209
+ clearTimeout(this.reconnectTimeout);
210
+ this.reconnectTimeout = undefined;
211
+ }
212
+ // Remove all event listeners
213
+ if (this.client) {
214
+ this.client.removeAllListeners();
215
+ // Destroy the socket
216
+ if (!this.client.destroyed) {
217
+ this.client.destroy();
218
+ }
219
+ }
220
+ }
221
+ }