redux-cluster-ws 2.0.1 → 2.0.2
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/package.json +7 -2
- package/FUNDING.yml +0 -7
- package/eslint.config.js +0 -143
- package/examples/browser-example.cjs +0 -350
- package/examples/browser.html +0 -255
- package/examples/client.cjs +0 -155
- package/examples/cross-library-browser.html +0 -655
- package/examples/cross-library-client.cjs +0 -190
- package/examples/cross-library-server.cjs +0 -213
- package/examples/server.cjs +0 -96
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redux-cluster-ws",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "WebSocket client/server wrapper for redux-cluster with TypeScript support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"url": "https://github.com/siarheidudko/redux-cluster-ws/issues"
|
|
77
77
|
},
|
|
78
78
|
"homepage": "https://github.com/siarheidudko/redux-cluster-ws#readme",
|
|
79
|
+
"files": [
|
|
80
|
+
"dist",
|
|
81
|
+
"README.md",
|
|
82
|
+
"LICENSE"
|
|
83
|
+
],
|
|
79
84
|
"funding": [
|
|
80
85
|
{
|
|
81
86
|
"type": "individual",
|
|
@@ -96,7 +101,7 @@
|
|
|
96
101
|
],
|
|
97
102
|
"dependencies": {
|
|
98
103
|
"@sergdudko/objectstream": "^3.2.26",
|
|
99
|
-
"protoobject": "^
|
|
104
|
+
"protoobject": "^2.0.0",
|
|
100
105
|
"redux": "^5.0.1",
|
|
101
106
|
"redux-cluster": "^2.0.2"
|
|
102
107
|
},
|
package/FUNDING.yml
DELETED
package/eslint.config.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import js from "@eslint/js";
|
|
2
|
-
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
|
3
|
-
import tsParser from "@typescript-eslint/parser";
|
|
4
|
-
|
|
5
|
-
export default [
|
|
6
|
-
js.configs.recommended,
|
|
7
|
-
{
|
|
8
|
-
ignores: ["*.md", "**/*.md"], // Ignore Markdown files
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
files: ["src/**/*.ts"],
|
|
12
|
-
languageOptions: {
|
|
13
|
-
parser: tsParser,
|
|
14
|
-
parserOptions: {
|
|
15
|
-
ecmaVersion: 2020,
|
|
16
|
-
sourceType: "module",
|
|
17
|
-
project: "./tsconfig.json",
|
|
18
|
-
},
|
|
19
|
-
globals: {
|
|
20
|
-
// Node.js globals
|
|
21
|
-
global: "readonly",
|
|
22
|
-
process: "readonly",
|
|
23
|
-
console: "readonly",
|
|
24
|
-
Buffer: "readonly",
|
|
25
|
-
require: "readonly",
|
|
26
|
-
module: "readonly",
|
|
27
|
-
exports: "readonly",
|
|
28
|
-
__dirname: "readonly",
|
|
29
|
-
__filename: "readonly",
|
|
30
|
-
setTimeout: "readonly",
|
|
31
|
-
clearTimeout: "readonly",
|
|
32
|
-
setInterval: "readonly",
|
|
33
|
-
clearInterval: "readonly",
|
|
34
|
-
setImmediate: "readonly",
|
|
35
|
-
clearImmediate: "readonly",
|
|
36
|
-
structuredClone: "readonly",
|
|
37
|
-
// TypeScript globals
|
|
38
|
-
NodeJS: "readonly",
|
|
39
|
-
BufferEncoding: "readonly",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
plugins: {
|
|
43
|
-
"@typescript-eslint": tsPlugin,
|
|
44
|
-
},
|
|
45
|
-
rules: {
|
|
46
|
-
...tsPlugin.configs.recommended.rules,
|
|
47
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
48
|
-
"@typescript-eslint/no-unused-vars": [
|
|
49
|
-
"error",
|
|
50
|
-
{ argsIgnorePattern: "^_" },
|
|
51
|
-
],
|
|
52
|
-
"@typescript-eslint/explicit-function-return-type": "off",
|
|
53
|
-
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
54
|
-
"@typescript-eslint/no-empty-function": "off",
|
|
55
|
-
"no-case-declarations": "off",
|
|
56
|
-
"no-undef": "off", // TypeScript handles this
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
files: ["tests/**/*.cjs", "*.test.cjs", "test.*.cjs"],
|
|
61
|
-
languageOptions: {
|
|
62
|
-
parser: tsParser,
|
|
63
|
-
parserOptions: {
|
|
64
|
-
ecmaVersion: 2020,
|
|
65
|
-
sourceType: "script",
|
|
66
|
-
},
|
|
67
|
-
globals: {
|
|
68
|
-
// Node.js globals
|
|
69
|
-
global: "readonly",
|
|
70
|
-
process: "readonly",
|
|
71
|
-
console: "readonly",
|
|
72
|
-
Buffer: "readonly",
|
|
73
|
-
require: "readonly",
|
|
74
|
-
module: "readonly",
|
|
75
|
-
exports: "readonly",
|
|
76
|
-
__dirname: "readonly",
|
|
77
|
-
__filename: "readonly",
|
|
78
|
-
setTimeout: "readonly",
|
|
79
|
-
clearTimeout: "readonly",
|
|
80
|
-
setInterval: "readonly",
|
|
81
|
-
clearInterval: "readonly",
|
|
82
|
-
setImmediate: "readonly",
|
|
83
|
-
clearImmediate: "readonly",
|
|
84
|
-
structuredClone: "readonly",
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
plugins: {
|
|
88
|
-
"@typescript-eslint": tsPlugin,
|
|
89
|
-
},
|
|
90
|
-
rules: {
|
|
91
|
-
...tsPlugin.configs.recommended.rules,
|
|
92
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
93
|
-
"no-case-declarations": "off",
|
|
94
|
-
"no-undef": "off",
|
|
95
|
-
// Allow console in test files
|
|
96
|
-
"no-console": "off",
|
|
97
|
-
// Allow require in CommonJS
|
|
98
|
-
"@typescript-eslint/no-var-requires": "off",
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
files: ["examples/**/*.cjs", "*.cjs"],
|
|
103
|
-
languageOptions: {
|
|
104
|
-
parser: tsParser,
|
|
105
|
-
parserOptions: {
|
|
106
|
-
ecmaVersion: 2020,
|
|
107
|
-
sourceType: "script",
|
|
108
|
-
},
|
|
109
|
-
globals: {
|
|
110
|
-
// Node.js globals
|
|
111
|
-
global: "readonly",
|
|
112
|
-
process: "readonly",
|
|
113
|
-
console: "readonly",
|
|
114
|
-
Buffer: "readonly",
|
|
115
|
-
require: "readonly",
|
|
116
|
-
module: "readonly",
|
|
117
|
-
exports: "readonly",
|
|
118
|
-
__dirname: "readonly",
|
|
119
|
-
__filename: "readonly",
|
|
120
|
-
setTimeout: "readonly",
|
|
121
|
-
clearTimeout: "readonly",
|
|
122
|
-
setInterval: "readonly",
|
|
123
|
-
clearInterval: "readonly",
|
|
124
|
-
setImmediate: "readonly",
|
|
125
|
-
clearImmediate: "readonly",
|
|
126
|
-
structuredClone: "readonly",
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
plugins: {
|
|
130
|
-
"@typescript-eslint": tsPlugin,
|
|
131
|
-
},
|
|
132
|
-
rules: {
|
|
133
|
-
...tsPlugin.configs.recommended.rules,
|
|
134
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
135
|
-
"no-case-declarations": "off",
|
|
136
|
-
"no-undef": "off",
|
|
137
|
-
// Allow console in examples
|
|
138
|
-
"no-console": "off",
|
|
139
|
-
// Allow require in CommonJS
|
|
140
|
-
"@typescript-eslint/no-var-requires": "off",
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
];
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Redux-Cluster-WS Browser Example JavaScript
|
|
3
|
-
*
|
|
4
|
-
* This script demonstrates how to use redux-cluster-ws in a browser environment.
|
|
5
|
-
* It creates a simple counter application that synchronizes with a WebSocket server.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// Store instance
|
|
9
|
-
let store = null;
|
|
10
|
-
let wsClient = null;
|
|
11
|
-
|
|
12
|
-
// Simple Redux implementation for browser (minimal version)
|
|
13
|
-
function createSimpleRedux(reducer) {
|
|
14
|
-
let state = reducer(undefined, {});
|
|
15
|
-
let listeners = [];
|
|
16
|
-
|
|
17
|
-
return {
|
|
18
|
-
getState: () => state,
|
|
19
|
-
dispatch: (action) => {
|
|
20
|
-
state = reducer(state, action);
|
|
21
|
-
listeners.forEach((listener) => listener());
|
|
22
|
-
return action;
|
|
23
|
-
},
|
|
24
|
-
subscribe: (listener) => {
|
|
25
|
-
listeners.push(listener);
|
|
26
|
-
return () => {
|
|
27
|
-
listeners = listeners.filter((l) => l !== listener);
|
|
28
|
-
};
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Counter reducer (same as server)
|
|
34
|
-
function counterReducer(state = { count: 0, lastUpdate: null }, action) {
|
|
35
|
-
switch (action.type) {
|
|
36
|
-
case "INCREMENT":
|
|
37
|
-
return {
|
|
38
|
-
count: state.count + 1,
|
|
39
|
-
lastUpdate: new Date().toISOString(),
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
case "DECREMENT":
|
|
43
|
-
return {
|
|
44
|
-
count: state.count - 1,
|
|
45
|
-
lastUpdate: new Date().toISOString(),
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
case "RESET":
|
|
49
|
-
return {
|
|
50
|
-
count: 0,
|
|
51
|
-
lastUpdate: new Date().toISOString(),
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
default:
|
|
55
|
-
return state;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Simple hash function (simplified version of redux-cluster-ws hasher)
|
|
60
|
-
function simpleHash(str) {
|
|
61
|
-
let hash = 0;
|
|
62
|
-
for (let i = 0; i < str.length; i++) {
|
|
63
|
-
const char = str.charCodeAt(i);
|
|
64
|
-
hash = (hash << 5) - hash + char;
|
|
65
|
-
hash = hash & hash;
|
|
66
|
-
}
|
|
67
|
-
return hash.toString(16);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// WebSocket Client implementation
|
|
71
|
-
class BrowserWSClient {
|
|
72
|
-
constructor(store, config) {
|
|
73
|
-
this.store = store;
|
|
74
|
-
this.config = config;
|
|
75
|
-
this.authenticated = false;
|
|
76
|
-
this.login = simpleHash(`REDUX_CLUSTER${config.login}`);
|
|
77
|
-
this.password = simpleHash(`REDUX_CLUSTER${config.password}`);
|
|
78
|
-
this.originalDispatch = store.dispatch;
|
|
79
|
-
|
|
80
|
-
// Override store dispatch
|
|
81
|
-
this.store.dispatch = this.dispatch.bind(this);
|
|
82
|
-
this.store.connected = false;
|
|
83
|
-
this.store.RCHash = "browser-example-hash";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
connect() {
|
|
87
|
-
const url = `${this.config.host}:${this.config.port}/redux-cluster-${this.store.RCHash}`;
|
|
88
|
-
log(`Connecting to: ${url}`, "info");
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
this.ws = new WebSocket(url);
|
|
92
|
-
|
|
93
|
-
this.ws.onopen = () => {
|
|
94
|
-
log("WebSocket connected", "success");
|
|
95
|
-
this.sendMessage({
|
|
96
|
-
_msg: "REDUX_CLUSTER_SOCKET_AUTH",
|
|
97
|
-
_hash: this.store.RCHash,
|
|
98
|
-
_login: this.login,
|
|
99
|
-
_password: this.password,
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
this.ws.onmessage = (event) => {
|
|
104
|
-
try {
|
|
105
|
-
const message = JSON.parse(event.data);
|
|
106
|
-
this.handleMessage(message);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
log(`Message parse error: ${error.message}`, "error");
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
this.ws.onclose = () => {
|
|
113
|
-
log("WebSocket disconnected", "warning");
|
|
114
|
-
this.authenticated = false;
|
|
115
|
-
this.store.connected = false;
|
|
116
|
-
updateUI();
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
this.ws.onerror = (error) => {
|
|
120
|
-
log(`WebSocket error: ${error}`, "error");
|
|
121
|
-
this.authenticated = false;
|
|
122
|
-
this.store.connected = false;
|
|
123
|
-
updateUI();
|
|
124
|
-
};
|
|
125
|
-
} catch (error) {
|
|
126
|
-
log(`Connection error: ${error.message}`, "error");
|
|
127
|
-
this.store.connected = false;
|
|
128
|
-
updateUI();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
handleMessage(message) {
|
|
133
|
-
if (message._hash !== this.store.RCHash) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
switch (message._msg) {
|
|
138
|
-
case "REDUX_CLUSTER_MSGTOWORKER":
|
|
139
|
-
if (message._action) {
|
|
140
|
-
log(`Received action: ${message._action.type}`, "info");
|
|
141
|
-
this.originalDispatch(message._action);
|
|
142
|
-
}
|
|
143
|
-
break;
|
|
144
|
-
|
|
145
|
-
case "REDUX_CLUSTER_SOCKET_AUTHSTATE":
|
|
146
|
-
if (message._value === true) {
|
|
147
|
-
this.authenticated = true;
|
|
148
|
-
this.store.connected = true;
|
|
149
|
-
log("Authentication successful", "success");
|
|
150
|
-
updateUI();
|
|
151
|
-
|
|
152
|
-
// Request initial sync
|
|
153
|
-
this.sendMessage({
|
|
154
|
-
_msg: "REDUX_CLUSTER_START",
|
|
155
|
-
_hash: this.store.RCHash,
|
|
156
|
-
});
|
|
157
|
-
} else {
|
|
158
|
-
this.authenticated = false;
|
|
159
|
-
this.store.connected = false;
|
|
160
|
-
|
|
161
|
-
if (message._banned) {
|
|
162
|
-
log("Authentication failed: IP banned", "error");
|
|
163
|
-
} else {
|
|
164
|
-
log("Authentication failed: Invalid credentials", "error");
|
|
165
|
-
}
|
|
166
|
-
updateUI();
|
|
167
|
-
}
|
|
168
|
-
break;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
dispatch(action) {
|
|
173
|
-
try {
|
|
174
|
-
if (
|
|
175
|
-
this.ws &&
|
|
176
|
-
this.ws.readyState === WebSocket.OPEN &&
|
|
177
|
-
this.authenticated
|
|
178
|
-
) {
|
|
179
|
-
log(`Sending action: ${action.type}`, "info");
|
|
180
|
-
this.sendMessage({
|
|
181
|
-
_msg: "REDUX_CLUSTER_MSGTOMASTER",
|
|
182
|
-
_hash: this.store.RCHash,
|
|
183
|
-
_action: action,
|
|
184
|
-
});
|
|
185
|
-
} else {
|
|
186
|
-
log("Cannot dispatch: not connected or not authenticated", "warning");
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
log(`Dispatch error: ${error.message}`, "error");
|
|
190
|
-
}
|
|
191
|
-
return action;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
sendMessage(message) {
|
|
195
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
196
|
-
this.ws.send(JSON.stringify(message));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
disconnect() {
|
|
201
|
-
if (this.ws) {
|
|
202
|
-
this.ws.close();
|
|
203
|
-
}
|
|
204
|
-
this.store.dispatch = this.originalDispatch;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Logging function
|
|
209
|
-
function log(message, type = "info") {
|
|
210
|
-
const logEl = document.getElementById("log");
|
|
211
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
212
|
-
const entry = document.createElement("div");
|
|
213
|
-
entry.className = `log-entry log-${type}`;
|
|
214
|
-
entry.textContent = `[${timestamp}] ${message}`;
|
|
215
|
-
|
|
216
|
-
logEl.appendChild(entry);
|
|
217
|
-
logEl.scrollTop = logEl.scrollHeight;
|
|
218
|
-
|
|
219
|
-
// Keep only last 50 entries
|
|
220
|
-
while (logEl.children.length > 50) {
|
|
221
|
-
logEl.removeChild(logEl.firstChild);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// UI update function
|
|
226
|
-
function updateUI() {
|
|
227
|
-
const statusEl = document.getElementById("status");
|
|
228
|
-
const connectBtn = document.getElementById("connectBtn");
|
|
229
|
-
const counterEl = document.getElementById("counterValue");
|
|
230
|
-
const lastUpdateEl = document.getElementById("lastUpdate");
|
|
231
|
-
const buttons = ["incBtn", "decBtn", "resetBtn"];
|
|
232
|
-
|
|
233
|
-
if (store && store.connected) {
|
|
234
|
-
statusEl.textContent = "Connected";
|
|
235
|
-
statusEl.className = "status connected";
|
|
236
|
-
connectBtn.textContent = "Disconnect";
|
|
237
|
-
|
|
238
|
-
// Enable action buttons
|
|
239
|
-
buttons.forEach((id) => {
|
|
240
|
-
document.getElementById(id).disabled = false;
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Update counter display
|
|
244
|
-
const state = store.getState();
|
|
245
|
-
counterEl.textContent = state.count;
|
|
246
|
-
lastUpdateEl.textContent = state.lastUpdate
|
|
247
|
-
? `Last updated: ${new Date(state.lastUpdate).toLocaleString()}`
|
|
248
|
-
: "Never updated";
|
|
249
|
-
} else if (store) {
|
|
250
|
-
statusEl.textContent = "Connecting...";
|
|
251
|
-
statusEl.className = "status connecting";
|
|
252
|
-
connectBtn.textContent = "Cancel";
|
|
253
|
-
|
|
254
|
-
// Disable action buttons
|
|
255
|
-
buttons.forEach((id) => {
|
|
256
|
-
document.getElementById(id).disabled = true;
|
|
257
|
-
});
|
|
258
|
-
} else {
|
|
259
|
-
statusEl.textContent = "Disconnected";
|
|
260
|
-
statusEl.className = "status disconnected";
|
|
261
|
-
connectBtn.textContent = "Connect";
|
|
262
|
-
|
|
263
|
-
// Disable action buttons
|
|
264
|
-
buttons.forEach((id) => {
|
|
265
|
-
document.getElementById(id).disabled = true;
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
counterEl.textContent = "0";
|
|
269
|
-
lastUpdateEl.textContent = "Never updated";
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Connection toggle function
|
|
274
|
-
function toggleConnection() {
|
|
275
|
-
if (store && store.connected) {
|
|
276
|
-
// Disconnect
|
|
277
|
-
log("Disconnecting...", "info");
|
|
278
|
-
if (wsClient) {
|
|
279
|
-
wsClient.disconnect();
|
|
280
|
-
}
|
|
281
|
-
store = null;
|
|
282
|
-
wsClient = null;
|
|
283
|
-
updateUI();
|
|
284
|
-
} else if (store && !store.connected) {
|
|
285
|
-
// Cancel connection attempt
|
|
286
|
-
if (wsClient) {
|
|
287
|
-
wsClient.disconnect();
|
|
288
|
-
}
|
|
289
|
-
store = null;
|
|
290
|
-
wsClient = null;
|
|
291
|
-
updateUI();
|
|
292
|
-
} else {
|
|
293
|
-
// Connect
|
|
294
|
-
const host = document.getElementById("host").value.trim();
|
|
295
|
-
const port = document.getElementById("port").value.trim();
|
|
296
|
-
const login = document.getElementById("login").value.trim();
|
|
297
|
-
const password = document.getElementById("password").value.trim();
|
|
298
|
-
|
|
299
|
-
if (!host || !port || !login || !password) {
|
|
300
|
-
log("Please fill in all connection fields", "error");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
log("Creating store and connecting...", "info");
|
|
305
|
-
|
|
306
|
-
// Create store
|
|
307
|
-
store = createSimpleRedux(counterReducer);
|
|
308
|
-
|
|
309
|
-
// Subscribe to state changes
|
|
310
|
-
store.subscribe(() => {
|
|
311
|
-
updateUI();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Create WebSocket client
|
|
315
|
-
wsClient = new BrowserWSClient(store, {
|
|
316
|
-
host: host,
|
|
317
|
-
port: parseInt(port),
|
|
318
|
-
login: login,
|
|
319
|
-
password: password,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
updateUI();
|
|
323
|
-
wsClient.connect();
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Action functions
|
|
328
|
-
function increment() {
|
|
329
|
-
if (store && store.connected) {
|
|
330
|
-
store.dispatch({ type: "INCREMENT" });
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function decrement() {
|
|
335
|
-
if (store && store.connected) {
|
|
336
|
-
store.dispatch({ type: "DECREMENT" });
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function reset() {
|
|
341
|
-
if (store && store.connected) {
|
|
342
|
-
store.dispatch({ type: "RESET" });
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Initialize UI on page load
|
|
347
|
-
document.addEventListener("DOMContentLoaded", () => {
|
|
348
|
-
log("Browser example loaded", "success");
|
|
349
|
-
updateUI();
|
|
350
|
-
});
|