@vnejs/desktop 0.2.0 → 0.2.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 +1 -1
- package/src/server.js +174 -28
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -3,21 +3,37 @@ const os = require("os");
|
|
|
3
3
|
const fastify = require("fastify");
|
|
4
4
|
const fastifyCors = require("@fastify/cors");
|
|
5
5
|
const fastifyStatic = require("@fastify/static");
|
|
6
|
-
const { readFile, readdir, writeFile, appendFile, mkdir, unlink, access } = require("fs/promises");
|
|
6
|
+
const { readFile, readdir, writeFile, appendFile, mkdir, unlink, rmdir, access } = require("fs/promises");
|
|
7
7
|
const { default: getPort } = require("get-port");
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
const dot = key.indexOf(".");
|
|
9
|
+
const STORAGE_DEFAULT_GROUP = "_";
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
12
|
+
|
|
13
|
+
const getStateRelPath = (key) => {
|
|
14
|
+
const first = key.indexOf("/");
|
|
15
|
+
|
|
16
|
+
if (first === -1) return `${key}.json`;
|
|
17
|
+
|
|
18
|
+
const module = key.slice(0, first);
|
|
19
|
+
const afterModule = key.slice(first + 1);
|
|
20
|
+
const second = afterModule.indexOf("/");
|
|
21
|
+
|
|
22
|
+
if (second === -1) return `${module}.json`;
|
|
23
|
+
|
|
24
|
+
const group = afterModule.slice(0, second);
|
|
25
|
+
|
|
26
|
+
if (!group || group === STORAGE_DEFAULT_GROUP) return `${module}.json`;
|
|
27
|
+
|
|
28
|
+
return toPosixPath(path.join(module, `${group}.json`));
|
|
13
29
|
};
|
|
14
30
|
|
|
15
|
-
const
|
|
31
|
+
const splitStateByPath = (state) =>
|
|
16
32
|
Object.entries(state).reduce((groups, [key, value]) => {
|
|
17
|
-
const
|
|
33
|
+
const relPath = getStateRelPath(key);
|
|
18
34
|
|
|
19
|
-
groups[
|
|
20
|
-
groups[
|
|
35
|
+
groups[relPath] ??= {};
|
|
36
|
+
groups[relPath][key] = value;
|
|
21
37
|
|
|
22
38
|
return groups;
|
|
23
39
|
}, {});
|
|
@@ -62,43 +78,135 @@ const pathExists = async (target) => {
|
|
|
62
78
|
}
|
|
63
79
|
};
|
|
64
80
|
|
|
65
|
-
const
|
|
81
|
+
const readPackageVnejs = async (rootDir) => {
|
|
66
82
|
const pkg = JSON.parse(await readFile(path.join(rootDir, "package.json"), "utf8"));
|
|
67
|
-
|
|
83
|
+
|
|
84
|
+
return pkg.vnejs || {};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const readPortPreferred = async (rootDir) => {
|
|
88
|
+
const port = Number((await readPackageVnejs(rootDir)).portPreferred);
|
|
68
89
|
|
|
69
90
|
return Number.isInteger(port) && port > 0 ? port : undefined;
|
|
70
91
|
};
|
|
71
92
|
|
|
72
|
-
const
|
|
93
|
+
const readDefaultLanguage = async (rootDir) => {
|
|
94
|
+
const language = (await readPackageVnejs(rootDir)).defaultLanguage;
|
|
95
|
+
|
|
96
|
+
return typeof language === "string" && language ? language : "ru";
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const runServer = async ({
|
|
100
|
+
rootDir,
|
|
101
|
+
stateDir,
|
|
102
|
+
oldApp,
|
|
103
|
+
onAchievementLock,
|
|
104
|
+
onAchievementUnlock,
|
|
105
|
+
onStateSave,
|
|
106
|
+
onStateGet,
|
|
107
|
+
onStateClear,
|
|
108
|
+
getConfig,
|
|
109
|
+
} = {}) => {
|
|
73
110
|
const port = await getPort({ port: await readPortPreferred(rootDir) });
|
|
74
111
|
|
|
75
112
|
const resolvedStateDir = stateDir || path.join(rootDir, "state");
|
|
76
113
|
const gamePath = path.join(rootDir, "game");
|
|
114
|
+
const defaultLanguage = await readDefaultLanguage(rootDir);
|
|
77
115
|
|
|
78
116
|
const ensureStateDir = () => mkdir(resolvedStateDir, { recursive: true });
|
|
79
117
|
|
|
80
|
-
const
|
|
118
|
+
const listStateJsonFiles = async (dir = resolvedStateDir) => {
|
|
81
119
|
try {
|
|
82
|
-
const
|
|
120
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
121
|
+
const files = await Promise.all(
|
|
122
|
+
entries.map(async (entry) => {
|
|
123
|
+
const fullPath = path.join(dir, entry.name);
|
|
124
|
+
|
|
125
|
+
if (entry.isDirectory()) return listStateJsonFiles(fullPath);
|
|
126
|
+
|
|
127
|
+
if (entry.isFile() && entry.name.endsWith(".json")) return [toPosixPath(path.relative(resolvedStateDir, fullPath))];
|
|
128
|
+
|
|
129
|
+
return [];
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
83
132
|
|
|
84
|
-
return files.
|
|
133
|
+
return files.flat();
|
|
85
134
|
} catch {
|
|
86
135
|
return [];
|
|
87
136
|
}
|
|
88
137
|
};
|
|
89
138
|
|
|
90
|
-
const
|
|
139
|
+
const removeEmptyStateDirs = async (dir = resolvedStateDir) => {
|
|
140
|
+
let entries;
|
|
141
|
+
|
|
91
142
|
try {
|
|
92
|
-
await
|
|
143
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await Promise.all(
|
|
149
|
+
entries
|
|
150
|
+
.filter((entry) => entry.isDirectory())
|
|
151
|
+
.map(async (entry) => {
|
|
152
|
+
const fullPath = path.join(dir, entry.name);
|
|
153
|
+
|
|
154
|
+
await removeEmptyStateDirs(fullPath);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await rmdir(fullPath);
|
|
158
|
+
} catch {
|
|
159
|
+
// directory is not empty
|
|
160
|
+
}
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const writeStateToDisk = async (state) => {
|
|
166
|
+
await ensureStateDir();
|
|
93
167
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
168
|
+
const groups = splitStateByPath(state);
|
|
169
|
+
const activePaths = new Set(Object.keys(groups));
|
|
170
|
+
const existingPaths = await listStateJsonFiles();
|
|
97
171
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
172
|
+
await Promise.all([
|
|
173
|
+
...Object.entries(groups).map(async ([relPath, groupState]) => {
|
|
174
|
+
const fullPath = path.join(resolvedStateDir, relPath);
|
|
175
|
+
|
|
176
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
177
|
+
await writeFile(fullPath, JSON.stringify(groupState));
|
|
178
|
+
}),
|
|
179
|
+
...existingPaths.filter((relPath) => !activePaths.has(relPath)).map((relPath) => unlink(path.join(resolvedStateDir, relPath))),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
await removeEmptyStateDirs();
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const readStateFromDisk = async () => {
|
|
186
|
+
const stateFiles = await listStateJsonFiles();
|
|
187
|
+
|
|
188
|
+
if (!stateFiles.length) return {};
|
|
189
|
+
|
|
190
|
+
const chunks = await Promise.all(stateFiles.map((file) => readFile(path.join(resolvedStateDir, file), "utf8").then(JSON.parse)));
|
|
191
|
+
|
|
192
|
+
return Object.assign({}, ...chunks);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const resolveConfigLanguage = () => {
|
|
196
|
+
if (typeof getConfig === "function") {
|
|
197
|
+
const config = getConfig() || {};
|
|
198
|
+
|
|
199
|
+
if (typeof config.language === "string" && config.language) return config.language;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return defaultLanguage;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const onSaveState = async (req, reply) => {
|
|
206
|
+
try {
|
|
207
|
+
await writeStateToDisk(req.body);
|
|
208
|
+
|
|
209
|
+
if (typeof onStateSave === "function") onStateSave(req.body);
|
|
102
210
|
|
|
103
211
|
return reply.send({ status: "ok" });
|
|
104
212
|
} catch {
|
|
@@ -107,14 +215,40 @@ const runServer = async ({ rootDir, stateDir, oldApp, onAchievementLock, onAchie
|
|
|
107
215
|
};
|
|
108
216
|
|
|
109
217
|
const onGetState = async (req, reply) => {
|
|
110
|
-
|
|
218
|
+
try {
|
|
219
|
+
let state = null;
|
|
220
|
+
let fromCloud = false;
|
|
221
|
+
|
|
222
|
+
if (typeof onStateGet === "function") {
|
|
223
|
+
const cloudState = onStateGet();
|
|
224
|
+
|
|
225
|
+
if (cloudState && typeof cloudState === "object") {
|
|
226
|
+
state = cloudState;
|
|
227
|
+
fromCloud = true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!state) state = await readStateFromDisk();
|
|
111
232
|
|
|
112
|
-
|
|
233
|
+
if (fromCloud) await writeStateToDisk(state);
|
|
113
234
|
|
|
235
|
+
return reply.send({ status: "ok", state });
|
|
236
|
+
} catch {
|
|
237
|
+
return reply.send({ status: "error" });
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const onGetConfig = (req, reply) => reply.send({ language: resolveConfigLanguage() });
|
|
242
|
+
|
|
243
|
+
const onClearState = async (req, reply) => {
|
|
114
244
|
try {
|
|
115
|
-
const
|
|
245
|
+
const stateFiles = await getStateFiles();
|
|
116
246
|
|
|
117
|
-
|
|
247
|
+
await Promise.all(stateFiles.map((file) => unlink(path.join(resolvedStateDir, file))));
|
|
248
|
+
|
|
249
|
+
if (typeof onStateClear === "function") onStateClear();
|
|
250
|
+
|
|
251
|
+
return reply.send({ status: "ok" });
|
|
118
252
|
} catch {
|
|
119
253
|
return reply.send({ status: "error" });
|
|
120
254
|
}
|
|
@@ -149,6 +283,8 @@ const runServer = async ({ rootDir, stateDir, oldApp, onAchievementLock, onAchie
|
|
|
149
283
|
|
|
150
284
|
app.post("/state/save", onSaveState);
|
|
151
285
|
app.get("/state/get", onGetState);
|
|
286
|
+
app.post("/state/clear", onClearState);
|
|
287
|
+
app.get("/config", onGetConfig);
|
|
152
288
|
app.post("/achievement/lock", onAchievementLockMiddleware);
|
|
153
289
|
app.post("/achievement/unlock", onAchievementUnlockMiddleware);
|
|
154
290
|
app.post("/log", onLog);
|
|
@@ -189,7 +325,17 @@ const runServer = async ({ rootDir, stateDir, oldApp, onAchievementLock, onAchie
|
|
|
189
325
|
if (error.code === "EADDRINUSE") {
|
|
190
326
|
console.log("server not started on port: ", port);
|
|
191
327
|
|
|
192
|
-
return runServer({
|
|
328
|
+
return runServer({
|
|
329
|
+
rootDir,
|
|
330
|
+
stateDir,
|
|
331
|
+
oldApp: app,
|
|
332
|
+
onAchievementLock,
|
|
333
|
+
onAchievementUnlock,
|
|
334
|
+
onStateSave,
|
|
335
|
+
onStateGet,
|
|
336
|
+
onStateClear,
|
|
337
|
+
getConfig,
|
|
338
|
+
});
|
|
193
339
|
}
|
|
194
340
|
|
|
195
341
|
throw error;
|