@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +174 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vnejs/desktop",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Tools to run desktop app of @vnejs",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 getPrefix = (key) => {
10
- const dot = key.indexOf(".");
9
+ const STORAGE_DEFAULT_GROUP = "_";
11
10
 
12
- return dot === -1 ? key : key.slice(0, dot);
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 splitStateByPrefix = (state) =>
31
+ const splitStateByPath = (state) =>
16
32
  Object.entries(state).reduce((groups, [key, value]) => {
17
- const prefix = getPrefix(key);
33
+ const relPath = getStateRelPath(key);
18
34
 
19
- groups[prefix] ??= {};
20
- groups[prefix][key] = value;
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 readPortPreferred = async (rootDir) => {
81
+ const readPackageVnejs = async (rootDir) => {
66
82
  const pkg = JSON.parse(await readFile(path.join(rootDir, "package.json"), "utf8"));
67
- const port = Number(pkg.vnejs?.portPreferred);
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 runServer = async ({ rootDir, stateDir, oldApp, onAchievementLock, onAchievementUnlock } = {}) => {
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 getStateFiles = async () => {
118
+ const listStateJsonFiles = async (dir = resolvedStateDir) => {
81
119
  try {
82
- const files = await readdir(resolvedStateDir);
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.filter((file) => file.endsWith(".json"));
133
+ return files.flat();
85
134
  } catch {
86
135
  return [];
87
136
  }
88
137
  };
89
138
 
90
- const onSaveState = async (req, reply) => {
139
+ const removeEmptyStateDirs = async (dir = resolvedStateDir) => {
140
+ let entries;
141
+
91
142
  try {
92
- await ensureStateDir();
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
- const groups = splitStateByPrefix(req.body);
95
- const activePrefixes = new Set(Object.keys(groups));
96
- const stalePrefixes = (await getStateFiles()).map((file) => file.slice(0, -".json".length)).filter((prefix) => !activePrefixes.has(prefix));
168
+ const groups = splitStateByPath(state);
169
+ const activePaths = new Set(Object.keys(groups));
170
+ const existingPaths = await listStateJsonFiles();
97
171
 
98
- await Promise.all([
99
- ...Object.entries(groups).map(([prefix, state]) => writeFile(path.join(resolvedStateDir, `${prefix}.json`), JSON.stringify(state))),
100
- ...stalePrefixes.map((prefix) => unlink(path.join(resolvedStateDir, `${prefix}.json`))),
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
- const stateFiles = await getStateFiles();
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
- if (!stateFiles.length) return reply.send({ status: "ok", state: {} });
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 chunks = await Promise.all(stateFiles.map((file) => readFile(path.join(resolvedStateDir, file), "utf8").then(JSON.parse)));
245
+ const stateFiles = await getStateFiles();
116
246
 
117
- return reply.send({ status: "ok", state: Object.assign({}, ...chunks) });
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({ rootDir, stateDir, oldApp: app, onAchievementLock, onAchievementUnlock });
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;