@djodjonx/x32-simulator 0.0.6 → 0.0.7
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/CHANGELOG.md +20 -0
- package/dist/{SchemaRegistry-D4eIJord.mjs → SchemaRegistry-BBzyicoy.mjs} +1271 -192
- package/dist/{SchemaRegistry-DE6iObDv.cjs → SchemaRegistry-Ddy5AEKs.cjs} +1282 -191
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +84 -39
- package/dist/index.d.mts +84 -39
- package/dist/index.mjs +2 -2
- package/dist/server.cjs +205 -10
- package/dist/server.mjs +206 -10
- package/package.json +1 -1
package/dist/server.cjs
CHANGED
|
@@ -1,12 +1,164 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_SchemaRegistry = require('./SchemaRegistry-
|
|
2
|
+
const require_SchemaRegistry = require('./SchemaRegistry-Ddy5AEKs.cjs');
|
|
3
3
|
let node_readline = require("node:readline");
|
|
4
4
|
node_readline = require_SchemaRegistry.__toESM(node_readline);
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
node_fs = require_SchemaRegistry.__toESM(node_fs);
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_SchemaRegistry.__toESM(node_path);
|
|
9
|
+
let node_fs_promises = require("node:fs/promises");
|
|
9
10
|
|
|
11
|
+
//#region src/infrastructure/repositories/JsonFileStateRepository.ts
|
|
12
|
+
/**
|
|
13
|
+
* JSON file-based state repository.
|
|
14
|
+
* Persists state to a JSON file on disk.
|
|
15
|
+
* Supports auto-save and manual flush.
|
|
16
|
+
*/
|
|
17
|
+
var JsonFileStateRepository = class {
|
|
18
|
+
state = /* @__PURE__ */ new Map();
|
|
19
|
+
dirty = false;
|
|
20
|
+
autoSaveTimer = null;
|
|
21
|
+
saving = false;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new JSON file repository.
|
|
24
|
+
* @param filePath - Path to the JSON file.
|
|
25
|
+
* @param autoSaveInterval - Auto-save interval in ms (0 = disabled).
|
|
26
|
+
*/
|
|
27
|
+
constructor(filePath, autoSaveInterval = 5e3) {
|
|
28
|
+
this.filePath = filePath;
|
|
29
|
+
this.autoSaveInterval = autoSaveInterval;
|
|
30
|
+
if (this.autoSaveInterval > 0) this.startAutoSave();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Starts the auto-save timer.
|
|
34
|
+
*/
|
|
35
|
+
startAutoSave() {
|
|
36
|
+
this.autoSaveTimer = setInterval(async () => {
|
|
37
|
+
if (this.dirty && !this.saving) await this.flush();
|
|
38
|
+
}, this.autoSaveInterval);
|
|
39
|
+
this.autoSaveTimer.unref();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Stops the auto-save timer.
|
|
43
|
+
*/
|
|
44
|
+
stopAutoSave() {
|
|
45
|
+
if (this.autoSaveTimer) {
|
|
46
|
+
clearInterval(this.autoSaveTimer);
|
|
47
|
+
this.autoSaveTimer = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks if the file exists.
|
|
52
|
+
*/
|
|
53
|
+
async fileExists() {
|
|
54
|
+
try {
|
|
55
|
+
await (0, node_fs_promises.access)(this.filePath, node_fs.constants.F_OK);
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Loads state from JSON file.
|
|
63
|
+
*/
|
|
64
|
+
async load() {
|
|
65
|
+
if (await this.fileExists()) try {
|
|
66
|
+
const content = await (0, node_fs_promises.readFile)(this.filePath, "utf-8");
|
|
67
|
+
const data = JSON.parse(content);
|
|
68
|
+
this.state.clear();
|
|
69
|
+
for (const [key, value] of Object.entries(data)) this.state.set(key, value);
|
|
70
|
+
this.dirty = false;
|
|
71
|
+
return data;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`[JsonFileStateRepository] Failed to load state from ${this.filePath}:`, error);
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Saves complete state to JSON file.
|
|
80
|
+
*/
|
|
81
|
+
async save(state) {
|
|
82
|
+
this.state.clear();
|
|
83
|
+
for (const [key, value] of Object.entries(state)) this.state.set(key, value);
|
|
84
|
+
this.dirty = true;
|
|
85
|
+
await this.flush();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Gets a single value.
|
|
89
|
+
*/
|
|
90
|
+
get(address) {
|
|
91
|
+
return this.state.get(address);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Sets a single value and marks as dirty.
|
|
95
|
+
*/
|
|
96
|
+
set(address, value) {
|
|
97
|
+
this.state.set(address, value);
|
|
98
|
+
this.dirty = true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Gets all entries.
|
|
102
|
+
*/
|
|
103
|
+
getAll() {
|
|
104
|
+
const result = {};
|
|
105
|
+
this.state.forEach((value, key) => {
|
|
106
|
+
result[key] = value;
|
|
107
|
+
});
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resets state to defaults and marks as dirty.
|
|
112
|
+
*/
|
|
113
|
+
reset(defaults) {
|
|
114
|
+
this.state.clear();
|
|
115
|
+
for (const [key, value] of Object.entries(defaults)) this.state.set(key, value);
|
|
116
|
+
this.dirty = true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Writes state to JSON file if dirty.
|
|
120
|
+
*/
|
|
121
|
+
async flush() {
|
|
122
|
+
if (!this.dirty || this.saving) return;
|
|
123
|
+
this.saving = true;
|
|
124
|
+
try {
|
|
125
|
+
const data = this.getAll();
|
|
126
|
+
const json = JSON.stringify(data, null, 2);
|
|
127
|
+
await (0, node_fs_promises.writeFile)(this.filePath, json, "utf-8");
|
|
128
|
+
this.dirty = false;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`[JsonFileStateRepository] Failed to save state to ${this.filePath}:`, error);
|
|
131
|
+
} finally {
|
|
132
|
+
this.saving = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Cleanup: stop auto-save and flush pending changes.
|
|
137
|
+
*/
|
|
138
|
+
async dispose() {
|
|
139
|
+
this.stopAutoSave();
|
|
140
|
+
await this.flush();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/infrastructure/repositories/StateRepositoryFactory.ts
|
|
146
|
+
/**
|
|
147
|
+
* Creates a state repository based on configuration.
|
|
148
|
+
* @param config - Repository configuration.
|
|
149
|
+
* @returns The configured state repository.
|
|
150
|
+
*/
|
|
151
|
+
function createStateRepository(config) {
|
|
152
|
+
switch (config.type) {
|
|
153
|
+
case "json":
|
|
154
|
+
if (!config.filePath) throw new Error("JSON repository requires a filePath");
|
|
155
|
+
return new JsonFileStateRepository(config.filePath, config.autoSaveInterval ?? 5e3);
|
|
156
|
+
case "memory":
|
|
157
|
+
default: return new require_SchemaRegistry.InMemoryStateRepository();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//#endregion
|
|
10
162
|
//#region src/presentation/cli/server.ts
|
|
11
163
|
const loadEnv = () => {
|
|
12
164
|
try {
|
|
@@ -28,6 +180,8 @@ const parseArgs = (argv) => {
|
|
|
28
180
|
const args = argv.slice(2);
|
|
29
181
|
let cliPort;
|
|
30
182
|
let cliHost;
|
|
183
|
+
let storageType = "memory";
|
|
184
|
+
let stateFilePath;
|
|
31
185
|
for (let i = 0; i < args.length; i++) {
|
|
32
186
|
const arg = args[i];
|
|
33
187
|
if (arg === "--port" || arg === "-p") {
|
|
@@ -46,29 +200,64 @@ const parseArgs = (argv) => {
|
|
|
46
200
|
continue;
|
|
47
201
|
}
|
|
48
202
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
203
|
+
if (arg === "--storage" || arg === "-s") {
|
|
204
|
+
const next = args[i + 1];
|
|
205
|
+
if (next && (next === "memory" || next === "json")) {
|
|
206
|
+
storageType = next;
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
52
209
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
210
|
+
if (arg === "--state-file" || arg === "-f") {
|
|
211
|
+
const next = args[i + 1];
|
|
212
|
+
if (next) {
|
|
213
|
+
stateFilePath = next;
|
|
214
|
+
storageType = "json";
|
|
215
|
+
i++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (arg === "--help") {
|
|
219
|
+
console.log(`
|
|
220
|
+
X32 Simulator - Usage:
|
|
221
|
+
x32-simulator [options]
|
|
222
|
+
|
|
223
|
+
Options:
|
|
224
|
+
-p, --port <port> UDP port to listen on (default: 10023)
|
|
225
|
+
-h, --host <ip> IP address to bind to (default: 0.0.0.0)
|
|
226
|
+
-s, --storage <type> Storage type: 'memory' or 'json' (default: memory)
|
|
227
|
+
-f, --state-file <file> Path to JSON state file (implies --storage json)
|
|
228
|
+
--help Show this help message
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
x32-simulator --port 10023 --host 192.168.1.100
|
|
232
|
+
x32-simulator --storage json --state-file ./x32-state.json
|
|
233
|
+
x32-simulator -p 10023 -f ./my-mixer-state.json
|
|
234
|
+
`);
|
|
235
|
+
process.exit(0);
|
|
56
236
|
}
|
|
237
|
+
if (!cliHost && /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(arg)) cliHost = arg;
|
|
238
|
+
if (!cliPort && /^\d+$/.test(arg)) cliPort = parseInt(arg, 10);
|
|
57
239
|
}
|
|
58
240
|
return {
|
|
59
241
|
PORT: cliPort || parseInt(process.env.X32_PORT || "10023", 10),
|
|
60
|
-
HOST: cliHost || process.env.X32_IP || "0.0.0.0"
|
|
242
|
+
HOST: cliHost || process.env.X32_IP || "0.0.0.0",
|
|
243
|
+
storageType,
|
|
244
|
+
stateFilePath
|
|
61
245
|
};
|
|
62
246
|
};
|
|
63
247
|
const bootstrap = async () => {
|
|
64
248
|
loadEnv();
|
|
65
|
-
const { PORT, HOST } = parseArgs(process.argv);
|
|
249
|
+
const { PORT, HOST, storageType, stateFilePath } = parseArgs(process.argv);
|
|
66
250
|
const logger = require_SchemaRegistry.ConsoleLogger.getInstance();
|
|
67
251
|
logger.setLevel(require_SchemaRegistry.LogLevel.DEBUG);
|
|
68
252
|
const schemaRegistry = new require_SchemaRegistry.SchemaRegistry(new require_SchemaRegistry.SchemaFactory());
|
|
69
|
-
const service = new require_SchemaRegistry.SimulationService(new require_SchemaRegistry.UdpNetworkGateway(logger, new require_SchemaRegistry.OscCodec(schemaRegistry)), logger,
|
|
253
|
+
const service = new require_SchemaRegistry.SimulationService(new require_SchemaRegistry.UdpNetworkGateway(logger, new require_SchemaRegistry.OscCodec(schemaRegistry)), logger, createStateRepository({
|
|
254
|
+
type: storageType,
|
|
255
|
+
filePath: stateFilePath,
|
|
256
|
+
autoSaveInterval: 5e3
|
|
257
|
+
}), schemaRegistry, PORT, HOST);
|
|
70
258
|
try {
|
|
71
259
|
await service.start();
|
|
260
|
+
const storageInfo = storageType === "json" ? `JSON (${stateFilePath})` : "In-Memory";
|
|
72
261
|
console.log(`
|
|
73
262
|
╔════════════════════════════════════════╗
|
|
74
263
|
║ 🚀 X32 SIMULATION SERVER ACTIVE ║
|
|
@@ -76,10 +265,12 @@ const bootstrap = async () => {
|
|
|
76
265
|
• IP: ${HOST}
|
|
77
266
|
• Port: ${PORT}
|
|
78
267
|
• Protocol: UDP (Strict X32 Schema)
|
|
268
|
+
• Storage: ${storageInfo}
|
|
79
269
|
• Logging: ENHANCED (DEBUG)
|
|
80
270
|
|
|
81
271
|
👉 Type "exit" or "stop" to shut down.
|
|
82
272
|
👉 Type "reset" to reset faders to default.
|
|
273
|
+
👉 Type "save" to force save state (JSON mode).
|
|
83
274
|
`);
|
|
84
275
|
} catch (err) {
|
|
85
276
|
if (err.code === "EADDRNOTAVAIL") {
|
|
@@ -121,6 +312,10 @@ const bootstrap = async () => {
|
|
|
121
312
|
service.resetState();
|
|
122
313
|
console.log("✨ Console state reset to defaults.");
|
|
123
314
|
break;
|
|
315
|
+
case "save":
|
|
316
|
+
await service.saveState();
|
|
317
|
+
console.log("💾 State saved to storage.");
|
|
318
|
+
break;
|
|
124
319
|
default:
|
|
125
320
|
if (command) console.log(`Unknown command: "${command}"`);
|
|
126
321
|
break;
|
package/dist/server.mjs
CHANGED
|
@@ -1,9 +1,162 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as ConsoleLogger, i as UdpNetworkGateway, n as SchemaFactory, o as LogLevel, r as OscCodec, s as InMemoryStateRepository, t as SchemaRegistry
|
|
2
|
+
import { a as ConsoleLogger, i as UdpNetworkGateway, l as SimulationService, n as SchemaFactory, o as LogLevel, r as OscCodec, s as InMemoryStateRepository, t as SchemaRegistry } from "./SchemaRegistry-BBzyicoy.mjs";
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
+
import { constants } from "node:fs";
|
|
5
6
|
import * as path from "node:path";
|
|
7
|
+
import { access, readFile, writeFile } from "node:fs/promises";
|
|
6
8
|
|
|
9
|
+
//#region src/infrastructure/repositories/JsonFileStateRepository.ts
|
|
10
|
+
/**
|
|
11
|
+
* JSON file-based state repository.
|
|
12
|
+
* Persists state to a JSON file on disk.
|
|
13
|
+
* Supports auto-save and manual flush.
|
|
14
|
+
*/
|
|
15
|
+
var JsonFileStateRepository = class {
|
|
16
|
+
state = /* @__PURE__ */ new Map();
|
|
17
|
+
dirty = false;
|
|
18
|
+
autoSaveTimer = null;
|
|
19
|
+
saving = false;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new JSON file repository.
|
|
22
|
+
* @param filePath - Path to the JSON file.
|
|
23
|
+
* @param autoSaveInterval - Auto-save interval in ms (0 = disabled).
|
|
24
|
+
*/
|
|
25
|
+
constructor(filePath, autoSaveInterval = 5e3) {
|
|
26
|
+
this.filePath = filePath;
|
|
27
|
+
this.autoSaveInterval = autoSaveInterval;
|
|
28
|
+
if (this.autoSaveInterval > 0) this.startAutoSave();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Starts the auto-save timer.
|
|
32
|
+
*/
|
|
33
|
+
startAutoSave() {
|
|
34
|
+
this.autoSaveTimer = setInterval(async () => {
|
|
35
|
+
if (this.dirty && !this.saving) await this.flush();
|
|
36
|
+
}, this.autoSaveInterval);
|
|
37
|
+
this.autoSaveTimer.unref();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Stops the auto-save timer.
|
|
41
|
+
*/
|
|
42
|
+
stopAutoSave() {
|
|
43
|
+
if (this.autoSaveTimer) {
|
|
44
|
+
clearInterval(this.autoSaveTimer);
|
|
45
|
+
this.autoSaveTimer = null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Checks if the file exists.
|
|
50
|
+
*/
|
|
51
|
+
async fileExists() {
|
|
52
|
+
try {
|
|
53
|
+
await access(this.filePath, constants.F_OK);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Loads state from JSON file.
|
|
61
|
+
*/
|
|
62
|
+
async load() {
|
|
63
|
+
if (await this.fileExists()) try {
|
|
64
|
+
const content = await readFile(this.filePath, "utf-8");
|
|
65
|
+
const data = JSON.parse(content);
|
|
66
|
+
this.state.clear();
|
|
67
|
+
for (const [key, value] of Object.entries(data)) this.state.set(key, value);
|
|
68
|
+
this.dirty = false;
|
|
69
|
+
return data;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`[JsonFileStateRepository] Failed to load state from ${this.filePath}:`, error);
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Saves complete state to JSON file.
|
|
78
|
+
*/
|
|
79
|
+
async save(state) {
|
|
80
|
+
this.state.clear();
|
|
81
|
+
for (const [key, value] of Object.entries(state)) this.state.set(key, value);
|
|
82
|
+
this.dirty = true;
|
|
83
|
+
await this.flush();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Gets a single value.
|
|
87
|
+
*/
|
|
88
|
+
get(address) {
|
|
89
|
+
return this.state.get(address);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Sets a single value and marks as dirty.
|
|
93
|
+
*/
|
|
94
|
+
set(address, value) {
|
|
95
|
+
this.state.set(address, value);
|
|
96
|
+
this.dirty = true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Gets all entries.
|
|
100
|
+
*/
|
|
101
|
+
getAll() {
|
|
102
|
+
const result = {};
|
|
103
|
+
this.state.forEach((value, key) => {
|
|
104
|
+
result[key] = value;
|
|
105
|
+
});
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resets state to defaults and marks as dirty.
|
|
110
|
+
*/
|
|
111
|
+
reset(defaults) {
|
|
112
|
+
this.state.clear();
|
|
113
|
+
for (const [key, value] of Object.entries(defaults)) this.state.set(key, value);
|
|
114
|
+
this.dirty = true;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Writes state to JSON file if dirty.
|
|
118
|
+
*/
|
|
119
|
+
async flush() {
|
|
120
|
+
if (!this.dirty || this.saving) return;
|
|
121
|
+
this.saving = true;
|
|
122
|
+
try {
|
|
123
|
+
const data = this.getAll();
|
|
124
|
+
const json = JSON.stringify(data, null, 2);
|
|
125
|
+
await writeFile(this.filePath, json, "utf-8");
|
|
126
|
+
this.dirty = false;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`[JsonFileStateRepository] Failed to save state to ${this.filePath}:`, error);
|
|
129
|
+
} finally {
|
|
130
|
+
this.saving = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Cleanup: stop auto-save and flush pending changes.
|
|
135
|
+
*/
|
|
136
|
+
async dispose() {
|
|
137
|
+
this.stopAutoSave();
|
|
138
|
+
await this.flush();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/infrastructure/repositories/StateRepositoryFactory.ts
|
|
144
|
+
/**
|
|
145
|
+
* Creates a state repository based on configuration.
|
|
146
|
+
* @param config - Repository configuration.
|
|
147
|
+
* @returns The configured state repository.
|
|
148
|
+
*/
|
|
149
|
+
function createStateRepository(config) {
|
|
150
|
+
switch (config.type) {
|
|
151
|
+
case "json":
|
|
152
|
+
if (!config.filePath) throw new Error("JSON repository requires a filePath");
|
|
153
|
+
return new JsonFileStateRepository(config.filePath, config.autoSaveInterval ?? 5e3);
|
|
154
|
+
case "memory":
|
|
155
|
+
default: return new InMemoryStateRepository();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
7
160
|
//#region src/presentation/cli/server.ts
|
|
8
161
|
const loadEnv = () => {
|
|
9
162
|
try {
|
|
@@ -25,6 +178,8 @@ const parseArgs = (argv) => {
|
|
|
25
178
|
const args = argv.slice(2);
|
|
26
179
|
let cliPort;
|
|
27
180
|
let cliHost;
|
|
181
|
+
let storageType = "memory";
|
|
182
|
+
let stateFilePath;
|
|
28
183
|
for (let i = 0; i < args.length; i++) {
|
|
29
184
|
const arg = args[i];
|
|
30
185
|
if (arg === "--port" || arg === "-p") {
|
|
@@ -43,29 +198,64 @@ const parseArgs = (argv) => {
|
|
|
43
198
|
continue;
|
|
44
199
|
}
|
|
45
200
|
}
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
201
|
+
if (arg === "--storage" || arg === "-s") {
|
|
202
|
+
const next = args[i + 1];
|
|
203
|
+
if (next && (next === "memory" || next === "json")) {
|
|
204
|
+
storageType = next;
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
49
207
|
}
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
208
|
+
if (arg === "--state-file" || arg === "-f") {
|
|
209
|
+
const next = args[i + 1];
|
|
210
|
+
if (next) {
|
|
211
|
+
stateFilePath = next;
|
|
212
|
+
storageType = "json";
|
|
213
|
+
i++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (arg === "--help") {
|
|
217
|
+
console.log(`
|
|
218
|
+
X32 Simulator - Usage:
|
|
219
|
+
x32-simulator [options]
|
|
220
|
+
|
|
221
|
+
Options:
|
|
222
|
+
-p, --port <port> UDP port to listen on (default: 10023)
|
|
223
|
+
-h, --host <ip> IP address to bind to (default: 0.0.0.0)
|
|
224
|
+
-s, --storage <type> Storage type: 'memory' or 'json' (default: memory)
|
|
225
|
+
-f, --state-file <file> Path to JSON state file (implies --storage json)
|
|
226
|
+
--help Show this help message
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
x32-simulator --port 10023 --host 192.168.1.100
|
|
230
|
+
x32-simulator --storage json --state-file ./x32-state.json
|
|
231
|
+
x32-simulator -p 10023 -f ./my-mixer-state.json
|
|
232
|
+
`);
|
|
233
|
+
process.exit(0);
|
|
53
234
|
}
|
|
235
|
+
if (!cliHost && /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(arg)) cliHost = arg;
|
|
236
|
+
if (!cliPort && /^\d+$/.test(arg)) cliPort = parseInt(arg, 10);
|
|
54
237
|
}
|
|
55
238
|
return {
|
|
56
239
|
PORT: cliPort || parseInt(process.env.X32_PORT || "10023", 10),
|
|
57
|
-
HOST: cliHost || process.env.X32_IP || "0.0.0.0"
|
|
240
|
+
HOST: cliHost || process.env.X32_IP || "0.0.0.0",
|
|
241
|
+
storageType,
|
|
242
|
+
stateFilePath
|
|
58
243
|
};
|
|
59
244
|
};
|
|
60
245
|
const bootstrap = async () => {
|
|
61
246
|
loadEnv();
|
|
62
|
-
const { PORT, HOST } = parseArgs(process.argv);
|
|
247
|
+
const { PORT, HOST, storageType, stateFilePath } = parseArgs(process.argv);
|
|
63
248
|
const logger = ConsoleLogger.getInstance();
|
|
64
249
|
logger.setLevel(LogLevel.DEBUG);
|
|
65
250
|
const schemaRegistry = new SchemaRegistry(new SchemaFactory());
|
|
66
|
-
const service = new SimulationService(new UdpNetworkGateway(logger, new OscCodec(schemaRegistry)), logger,
|
|
251
|
+
const service = new SimulationService(new UdpNetworkGateway(logger, new OscCodec(schemaRegistry)), logger, createStateRepository({
|
|
252
|
+
type: storageType,
|
|
253
|
+
filePath: stateFilePath,
|
|
254
|
+
autoSaveInterval: 5e3
|
|
255
|
+
}), schemaRegistry, PORT, HOST);
|
|
67
256
|
try {
|
|
68
257
|
await service.start();
|
|
258
|
+
const storageInfo = storageType === "json" ? `JSON (${stateFilePath})` : "In-Memory";
|
|
69
259
|
console.log(`
|
|
70
260
|
╔════════════════════════════════════════╗
|
|
71
261
|
║ 🚀 X32 SIMULATION SERVER ACTIVE ║
|
|
@@ -73,10 +263,12 @@ const bootstrap = async () => {
|
|
|
73
263
|
• IP: ${HOST}
|
|
74
264
|
• Port: ${PORT}
|
|
75
265
|
• Protocol: UDP (Strict X32 Schema)
|
|
266
|
+
• Storage: ${storageInfo}
|
|
76
267
|
• Logging: ENHANCED (DEBUG)
|
|
77
268
|
|
|
78
269
|
👉 Type "exit" or "stop" to shut down.
|
|
79
270
|
👉 Type "reset" to reset faders to default.
|
|
271
|
+
👉 Type "save" to force save state (JSON mode).
|
|
80
272
|
`);
|
|
81
273
|
} catch (err) {
|
|
82
274
|
if (err.code === "EADDRNOTAVAIL") {
|
|
@@ -118,6 +310,10 @@ const bootstrap = async () => {
|
|
|
118
310
|
service.resetState();
|
|
119
311
|
console.log("✨ Console state reset to defaults.");
|
|
120
312
|
break;
|
|
313
|
+
case "save":
|
|
314
|
+
await service.saveState();
|
|
315
|
+
console.log("💾 State saved to storage.");
|
|
316
|
+
break;
|
|
121
317
|
default:
|
|
122
318
|
if (command) console.log(`Unknown command: "${command}"`);
|
|
123
319
|
break;
|