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.
- package/LICENSE +21 -21
- package/README.md +345 -471
- package/dist/cjs/core/backup.d.ts +10 -0
- package/dist/cjs/core/backup.d.ts.map +1 -0
- package/dist/cjs/core/backup.js +166 -0
- package/dist/cjs/core/redux-cluster.d.ts +47 -0
- package/dist/cjs/core/redux-cluster.d.ts.map +1 -0
- package/dist/cjs/core/redux-cluster.js +367 -0
- package/dist/cjs/index.d.ts +22 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +43 -0
- package/dist/cjs/network/client.d.ts +23 -0
- package/dist/cjs/network/client.d.ts.map +1 -0
- package/dist/cjs/network/client.js +251 -0
- package/dist/cjs/network/server.d.ts +39 -0
- package/dist/cjs/network/server.d.ts.map +1 -0
- package/dist/cjs/network/server.js +439 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/types/index.d.ts +125 -0
- package/dist/cjs/types/index.d.ts.map +1 -0
- package/dist/cjs/types/index.js +20 -0
- package/dist/cjs/utils/crypto.d.ts +22 -0
- package/dist/cjs/utils/crypto.d.ts.map +1 -0
- package/dist/cjs/utils/crypto.js +404 -0
- package/dist/esm/core/backup.d.ts +10 -0
- package/dist/esm/core/backup.d.ts.map +1 -0
- package/dist/esm/core/backup.js +134 -0
- package/dist/esm/core/redux-cluster.d.ts +47 -0
- package/dist/esm/core/redux-cluster.d.ts.map +1 -0
- package/dist/esm/core/redux-cluster.js +376 -0
- package/dist/esm/index.d.ts +22 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +25 -0
- package/dist/esm/network/client.d.ts +23 -0
- package/dist/esm/network/client.d.ts.map +1 -0
- package/dist/esm/network/client.js +221 -0
- package/dist/esm/network/server.d.ts +39 -0
- package/dist/esm/network/server.d.ts.map +1 -0
- package/dist/esm/network/server.js +408 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/types/index.d.ts +125 -0
- package/dist/esm/types/index.d.ts.map +1 -0
- package/dist/esm/types/index.js +17 -0
- package/dist/esm/utils/crypto.d.ts +22 -0
- package/dist/esm/utils/crypto.d.ts.map +1 -0
- package/dist/esm/utils/crypto.js +351 -0
- package/package.json +115 -34
- package/index.js +0 -678
- package/test.auto.js +0 -94
- package/test.auto.proc1.js +0 -97
- package/test.auto.proc2.js +0 -85
- package/test.visual.client.highload.js +0 -102
- package/test.visual.client.js +0 -103
- package/test.visual.error.js +0 -45
- package/test.visual.js +0 -97
- package/test.visual.server.highload.js +0 -102
- 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
|
+
}
|