jj.js 0.18.0 → 0.20.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/lib/types.js CHANGED
@@ -1,242 +1,409 @@
1
1
  const path = require('path');
2
+ const fs = require('fs');
2
3
  const fsPromises = require('fs').promises;
3
4
  const {app: cfg_app} = require('./config');
4
5
  const toHump = require('./utils/str').toHump;
5
6
  const Logger = require('./logger');
6
7
 
7
8
  /**
8
- * watch
9
+ * @typedef {Object} NodeInfo
10
+ * @property {string} node_name - 节点名称
11
+ * @property {string} [file_name] - 文件名
12
+ * @property {string} file_type - 文件类型 (file/dir)
13
+ * @property {boolean} [is_class] - 是否为类
14
+ * @property {Object<string, NodeInfo>} [children] - 子节点
9
15
  */
10
- function start() {
11
- const watch = require('watch');
12
- const filter = function(f, stat) {
13
- const static_dir = cfg_app.static_dir ? path.join(cfg_app.base_dir, cfg_app.static_dir) : '';
14
- return (!~f.indexOf('.') || /\.js(on)?$/.test(f)) && !~f.indexOf('types.js') && (static_dir && !~f.indexOf(static_dir));
15
- };
16
-
17
- let prevFile = {file: null,action: null,stat: null};
18
- watch.watchTree(cfg_app.base_dir, {ignoreDotFiles: true, ignoreDirectoryPattern: /node_modules/, filter}, (f, curr, prev) => {
19
- if(typeof f == "object" && prev === null && curr === null) {
20
- // Finished walking the tree
21
- Object.keys(f).forEach(file => {
22
- Logger.system("wacth:", file);
23
- });
24
- createFile();
25
- } else if(prev === null) {
26
- // f is a new file
27
- if(prevFile.file != f || prevFile.action != "created") {
28
- prevFile = {file: f, action: "created", stat: curr};
29
- createFile(f);
30
- }
31
- } else if(curr.nlink === 0) {
32
- // f was removed
33
- if(prevFile.file != f || prevFile.action != "removed") {
34
- prevFile = { file: f, action: "removed", stat: curr };
35
- createFile(f);
36
- }
37
- } else {
38
- // f was changed
39
- if(prevFile.file === null) {
40
- createFile(f);
41
- } else {
42
- // stat might return null, so catch errors
43
- try {
44
- if (prevFile.stat.mtime.getTime() !== curr.mtime.getTime()) {
45
- createFile(f);
46
- }
47
- } catch(e) {
48
- createFile(f);
49
- }
50
- }
51
- }
52
- });
53
- }
54
16
 
55
17
  /**
56
- * createTypesFile
18
+ * @typedef {Object} TypesGeneratorState
19
+ * @property {string|null} _f - 当前处理的文件路径
20
+ * @property {string} _APP - 当前应用名称
21
+ * @property {fs.FSWatcher|null} _watcher - 文件监听器
57
22
  */
58
- async function createFile(f) {
59
- if(f) {
60
- require.cache[f] && delete(require.cache[f])
61
- if(f == this._f) {
62
- return;
63
- }
23
+
24
+ /**
25
+ * @typedef {Object} PrevFile
26
+ * @property {string|null} file
27
+ * @property {string|null} action
28
+ * @property {import('fs').Stats|null} stat
29
+ */
30
+
31
+ /**
32
+ * TypesGenerator 类型定义生成器
33
+ */
34
+ class TypesGenerator {
35
+ constructor() {
36
+ /** @type {TypesGeneratorState} */
37
+ this.state = {
38
+ _f: null,
39
+ _APP: cfg_app.default_app,
40
+ _watcher: null
41
+ };
42
+ }
43
+
44
+ /**
45
+ * 启动文件监听并自动生成类型定义
46
+ * @returns {fs.FSWatcher|null}
47
+ */
48
+ watch() {
49
+ const filter = (/** @type {string} */ f) => {
50
+ const static_dir = cfg_app.static_dir ? path.join(cfg_app.base_dir, cfg_app.static_dir) : '';
51
+ if((/\.js(on)?$/.test(f)) && ~f.indexOf('types.js')) return false;
52
+ if(static_dir && ~f.indexOf(static_dir)) return false;
53
+ return true;
54
+ };
55
+
56
+ /** @type {PrevFile} */
57
+ let prevFile = {file: null, action: null, stat: null};
58
+ /** @type {NodeJS.Timeout|null} */
59
+ let debounceTimer = null;
60
+ const DEBOUNCE_DELAY = 300;
61
+
62
+ this.state._watcher = fs.watch(cfg_app.base_dir, { recursive: true }, (eventType, filename) => {
63
+ if (debounceTimer) {
64
+ clearTimeout(debounceTimer);
65
+ }
66
+
67
+ debounceTimer = setTimeout(() => {
68
+ this.handleFileChange(eventType, filename, filter, prevFile);
69
+ }, DEBOUNCE_DELAY);
70
+ });
71
+
72
+ // 初始扫描
73
+ this.scanDirectory(cfg_app.base_dir, (/** @type {string} */ file) => {
74
+ Logger.system("watch:", file);
75
+ }).then(() => {
76
+ this.createFile();
77
+ });
64
78
 
65
- this._f && require.cache[this._f] && delete(require.cache[this._f]);
66
- this._f = f;
67
-
68
- const f_info = f.replace(cfg_app.base_dir + path.sep, '').split(path.sep);
69
- if(cfg_app.app_multi && f_info[0] && f_info[1] && f_info[0] != cfg_app.common_app && f_info[0] != 'config' && !~f_info[0].indexOf('.')) {
70
- this._APP = f_info[0];
71
- }
72
- } else {
73
- this._APP = cfg_app.default_app;
79
+ return this.state._watcher;
74
80
  }
75
-
76
81
 
77
- const ignore = ['node_modules', 'docker', 'package-lock.json', 'types.js', '.git', '.gitignore'];
78
- ignore.push(path.basename(module.parent.parent.parent.filename));
79
- cfg_app.static_dir && ignore.push(path.join(cfg_app.base_dir, cfg_app.static_dir));
82
+ /**
83
+ * 处理文件变化回调
84
+ * @param {string} eventType - 事件类型 (rename/change)
85
+ * @param {string|null} filename - 文件名
86
+ * @param {function(string): boolean} filter - 过滤器函数
87
+ * @param {PrevFile} prevFile - 上一个文件信息
88
+ */
89
+ handleFileChange(eventType, filename, filter, prevFile) {
90
+ if (!filename) return;
91
+
92
+ const fullPath = path.join(cfg_app.base_dir, filename);
93
+
94
+ if (!filter(filename)) return;
95
+ if (filename.includes('node_modules')) return;
80
96
 
81
- const node_list = await getNodeList('.', ignore);
82
- calcNodeList(node_list, this._APP);
97
+ try {
98
+ const stat = fs.statSync(fullPath);
99
+
100
+ if (eventType === 'rename') {
101
+ if (fs.existsSync(fullPath)) {
102
+ if (prevFile.file != fullPath || prevFile.action != "created") {
103
+ prevFile.file = fullPath;
104
+ prevFile.action = "created";
105
+ prevFile.stat = stat;
106
+ this.createFile(fullPath);
107
+ }
108
+ } else {
109
+ if (prevFile.file != fullPath || prevFile.action != "removed") {
110
+ prevFile.file = fullPath;
111
+ prevFile.action = "removed";
112
+ prevFile.stat = stat;
113
+ this.createFile(fullPath);
114
+ }
115
+ }
116
+ } else if (eventType === 'change') {
117
+ if (prevFile.file === null || prevFile.file != fullPath) {
118
+ prevFile.file = fullPath;
119
+ prevFile.action = "change";
120
+ prevFile.stat = stat;
121
+ this.createFile(fullPath);
122
+ } else {
123
+ try {
124
+ if (!prevFile.stat || prevFile.stat.mtime.getTime() !== stat.mtime.getTime()) {
125
+ prevFile.file = fullPath;
126
+ prevFile.action = "change";
127
+ prevFile.stat = stat;
128
+ this.createFile(fullPath);
129
+ }
130
+ } catch(e) {
131
+ prevFile.file = fullPath;
132
+ prevFile.action = "change";
133
+ prevFile.stat = stat;
134
+ this.createFile(fullPath);
135
+ }
136
+ }
137
+ }
138
+ } catch (err) {
139
+ // 忽略统计错误
140
+ }
141
+ }
83
142
 
84
- const types_tpl = require('./tpl/types');
85
- const types_str = types_tpl.replace('__TYPES__', createTypes(node_list)).replace('__PROPERTY__', createProps(node_list));
86
- fsPromises.writeFile('types.js', types_str).then(_ => {
87
- Logger.system('createTypes success:', path.join(cfg_app.base_dir, 'types.js'));
88
- }).catch(err => {
89
- Logger.system('createTypes error:', err);
90
- });
91
- }
143
+ /**
144
+ * 递归扫描目录
145
+ * @param {string} dir
146
+ * @param {function(string): void} callback
147
+ * @param {string[]} ignoreDirs
148
+ * @returns {Promise<void>}
149
+ */
150
+ async scanDirectory(dir, callback, ignoreDirs = ['node_modules']) {
151
+ try {
152
+ const files = await fsPromises.readdir(dir, { withFileTypes: true });
153
+
154
+ for (const file of files) {
155
+ const fullPath = path.join(dir, file.name);
156
+
157
+ if (file.isDirectory()) {
158
+ if (!ignoreDirs.includes(file.name)) {
159
+ callback(fullPath);
160
+ await this.scanDirectory(fullPath, callback, ignoreDirs);
161
+ }
162
+ } else {
163
+ callback(fullPath);
164
+ }
165
+ }
166
+ } catch (err) {
167
+ Logger.system('scanDirectory error:', err);
168
+ }
169
+ }
92
170
 
93
- // 生成类型定义
94
- function createTypes(node_list, base_type = '') {
95
- let types = '';
96
- let props = '';
97
- Object.entries(node_list).forEach(([path, node]) => {
98
- const type_name = toHump(base_type + '_' + node.node_name);
99
- if(node.file_type == 'dir') {
100
- types += createTypes(node.children, type_name);
101
- } else {
102
- if(!node.is_class) {
103
- types += `/**\n * @typedef {typeof import('${path}')} ${type_name}\n */\n\n`;
104
- } else {
105
- types += `/**\n * @typedef {typeof import('${path}')} ${type_name}Class\n`;
106
- types += ` * @typedef {typeof import('${path}').prototype} ${type_name}Instance\n`;
107
- types += ` * @typedef {(${type_name}Class & ${type_name}Instance)} ${type_name}\n */\n\n`;
171
+ /**
172
+ * 创建类型定义文件
173
+ * @param {string} [f] - 触发变更的文件路径
174
+ * @returns {Promise<void>}
175
+ */
176
+ async createFile(f) {
177
+ if(f) {
178
+ require.cache[f] && delete(require.cache[f]);
179
+ if(f == this.state._f) {
180
+ return;
108
181
  }
182
+
183
+ this.state._f && require.cache[this.state._f] && delete(require.cache[this.state._f]);
184
+ this.state._f = f;
185
+
186
+ const f_info = f.replace(cfg_app.base_dir + path.sep, '').split(path.sep);
187
+ if(cfg_app.app_multi && f_info[0] && f_info[1] && f_info[0] != cfg_app.common_app && f_info[0] != 'config' && !~f_info[0].indexOf('.')) {
188
+ this.state._APP = f_info[0];
189
+ }
190
+ } else {
191
+ this.state._APP = cfg_app.default_app;
109
192
  }
193
+
110
194
 
111
- props += ` * @property {${type_name}} ${node.node_name} - ${path}\n`;
112
- });
195
+ const ignore = ['node_modules', 'docker', 'package-lock.json', 'types.js', '.git', '.gitignore'];
196
+ ignore.push(path.basename(require.main?.filename || 'server.js'));
197
+ cfg_app.static_dir && ignore.push(path.join(cfg_app.base_dir, cfg_app.static_dir));
113
198
 
114
- if(base_type) {
115
- types += `/**\n * @typedef {object} ${base_type}\n${props} */\n\n`;
199
+ const node_list = await this.getNodeList('.', ignore);
200
+ this.calcNodeList(node_list, this.state._APP);
201
+
202
+ const types_tpl = require('./tpl/types');
203
+ const types_str = types_tpl.replace('__TYPES__', this.createTypes(node_list)).replace('__PROPERTY__', this.createProps(node_list));
204
+ const types_js = path.join(cfg_app.base_dir, 'types.js');
205
+ fsPromises.writeFile(types_js, types_str).then(_ => {
206
+ Logger.system('createTypes success:', types_js);
207
+ }).catch(err => {
208
+ Logger.system('createTypes error:', err);
209
+ });
116
210
  }
117
211
 
118
- return types;
119
- }
212
+ /**
213
+ * 生成类型定义
214
+ * @param {Object<string, NodeInfo>} node_list
215
+ * @param {string} [base_type]
216
+ * @returns {string}
217
+ */
218
+ createTypes(node_list, base_type = '') {
219
+ let types = '';
220
+ let props = '';
221
+ Object.entries(node_list).forEach(([path, node]) => {
222
+ const type_name = toHump(base_type + '_' + node.node_name);
223
+ if(node.file_type == 'dir' && node.children) {
224
+ types += this.createTypes(node.children, type_name);
225
+ } else {
226
+ if(!node.is_class) {
227
+ types += `/**\n * @typedef {typeof import('${path}')} ${type_name}\n */\n\n`;
228
+ } else {
229
+ types += `/**\n * @typedef {typeof import('${path}')} ${type_name}Class\n`;
230
+ types += ` * @typedef {typeof import('${path}').prototype} ${type_name}Instance\n`;
231
+ types += ` * @typedef {(${type_name}Class & ${type_name}Instance)} ${type_name}\n */\n\n`;
232
+ }
233
+ }
120
234
 
121
- // 生成智能属性
122
- function createProps(node_list) {
123
- let props = '';
235
+ props += ` * @property {${type_name}} ${node.node_name} - ${path}\n`;
236
+ });
124
237
 
125
- Object.entries(node_list).forEach(([path, node]) => {
126
- const type_name = toHump('_' + node.node_name);
127
- if(type_name != 'Config') {
128
- props += `\n /** @type {${type_name}} */\n $${node.node_name}\n`;
238
+ if(base_type) {
239
+ types += `/**\n * @typedef {object} ${base_type}\n${props} */\n\n`;
129
240
  }
130
- });
131
241
 
132
- let type_config = 'JJConfig';
133
- if(node_list['./config']) {
134
- type_config = '(JJConfig & Config)';
242
+ return types;
135
243
  }
136
- props += `\n /** @type {${type_config}} */\n $config\n`;
137
244
 
138
- return props;
139
- }
245
+ /**
246
+ * 生成智能属性
247
+ * @param {Object<string, NodeInfo>} node_list
248
+ * @returns {string}
249
+ */
250
+ createProps(node_list) {
251
+ let props = '';
140
252
 
141
- // 获取节点对象
142
- async function getNodeList(dir, ignore=['node_modules', 'docker', 'package-lock.json', '.git', '.gitignore']) {
143
- const isClass = require('is-class');
144
- const files = await fsPromises.readdir(path.join(cfg_app.base_dir, dir), {withFileTypes: true});
145
- const type_list = {};
146
- for(const dirent of files) {
147
- const file_name = dirent.name;
148
- const file_path = dir + '/' + file_name;
149
- const node_name = path.parse(file_name).name;
150
- const abs_path = path.join(cfg_app.base_dir, file_path);
151
- if(ignore.filter(n => abs_path.includes(n)).length) {
152
- continue;
153
- }
253
+ Object.entries(node_list).forEach(([path, node]) => {
254
+ const type_name = toHump('_' + node.node_name);
255
+ if(type_name != 'Config') {
256
+ props += `\n /** @type {${type_name}} */\n $${node.node_name}\n`;
257
+ }
258
+ });
154
259
 
155
- const file_type = dirent.isFile() ? 'file' : dirent.isDirectory() ? 'dir' : '';
156
- const regFile = /.+\.js(on)?$/.test(file_name);
157
- const regDir = !file_name.includes('.');
158
- if(file_type == 'file' && !regFile) {
159
- continue;
160
- }
161
- if(file_type == 'dir' && !regDir) {
162
- continue;
260
+ let type_config = 'JJConfig';
261
+ if(node_list['./config']) {
262
+ type_config = '(JJConfig & Config)';
163
263
  }
264
+ props += `\n /** @type {${type_config}} */\n $config\n`;
164
265
 
165
- if(file_type == 'file') {
166
- try {
167
- // @ts-ignore
168
- type_list[file_path] = {node_name, file_name, file_type, is_class: isClass(require(abs_path))};
169
- } catch(e) {
170
- // 包含语法错误时,require会出错
266
+ return props;
267
+ }
268
+
269
+ /**
270
+ * 获取节点对象
271
+ * @param {string} dir
272
+ * @param {string[]} ignore
273
+ * @returns {Promise<Object<string, NodeInfo>>}
274
+ */
275
+ async getNodeList(dir, ignore = ['node_modules', 'docker', 'package-lock.json', '.git', '.gitignore']) {
276
+ const isClass = require('is-class');
277
+ const files = await fsPromises.readdir(path.join(cfg_app.base_dir, dir), {withFileTypes: true});
278
+ /** @type {Object<string, NodeInfo>} */
279
+ const type_list = {};
280
+ for(const dirent of files) {
281
+ const file_name = dirent.name;
282
+ const file_path = dir + '/' + file_name;
283
+ const node_name = path.parse(file_name).name;
284
+ const abs_path = path.join(cfg_app.base_dir, file_path);
285
+ if(ignore.filter(n => abs_path.includes(n)).length) {
286
+ continue;
171
287
  }
172
- } else if(file_type == 'dir') {
173
- type_list[file_path] = {node_name, file_name, file_type, children: await getNodeList(file_path, ignore)};;
174
- }
175
288
 
176
- };
177
- return type_list;
178
- }
289
+ const file_type = dirent.isFile() ? 'file' : dirent.isDirectory() ? 'dir' : '';
290
+ const regFile = /.+\.js(on)?$/.test(file_name);
291
+ const regDir = !file_name.includes('.');
292
+ if(file_type == 'file' && !regFile) {
293
+ continue;
294
+ }
295
+ if(file_type == 'dir' && !regDir) {
296
+ continue;
297
+ }
179
298
 
180
- // 复制子节点
181
- function copyNodeList(node_list) {
182
- const copy_node_list = {};
183
- Object.entries(node_list).forEach(([path, node]) => {
184
- copy_node_list[path] = {...node};
185
- if(node.children) {
186
- copy_node_list[path].children = copyNodeList(node.children);
299
+ if(file_type == 'file') {
300
+ try {
301
+ // @ts-ignore
302
+ type_list[file_path] = {node_name, file_name, file_type, is_class: isClass(require(abs_path))};
303
+ } catch(e) {
304
+ // 包含语法错误时,require会出错
305
+ }
306
+ } else if(file_type == 'dir') {
307
+ type_list[file_path] = {node_name, file_name, file_type, children: await this.getNodeList(file_path, ignore)};
308
+ }
187
309
  }
188
- });
189
- return copy_node_list;
190
- }
191
-
192
- // 处理common库
193
- function calcNodeList(node_list, _APP) {
194
- // 处理common
195
- const node_common = './' + cfg_app.common_app;
196
- if(cfg_app.common_app && node_list[node_common] && node_list[node_common].children) {
197
- const children = copyNodeList(node_list[node_common].children);
198
- Object.entries(children).forEach(([path, node]) => {
199
- node_list[path] = node;
200
- });
310
+ return type_list;
201
311
  }
202
312
 
203
- // 处理app
204
- const node_app = './' + _APP;
205
- if(_APP && node_list[node_app] && node_list[node_app].children) {
206
- Object.entries(node_list[node_app].children).forEach(([path, node]) => {
207
- const path_common = path.replace(node_app, node_common);
208
- if(!node_list[path_common]) {
209
- node_list[path] = node;
210
- } else if(node_list[path_common].file_type == 'file') {
211
- delete node_list[path_common];
212
- node_list[path] = node;
213
- } else if(node.children) {
214
- Object.entries(node.children).forEach(([p, n]) => {
215
- const p_commmon = p.replace(node_app, node_common);
216
- node_list[path_common].children[p_commmon] && delete node_list[path_common].children[p_commmon];
217
- node_list[path_common].children[p] = n;
218
- });
313
+ /**
314
+ * 复制子节点
315
+ * @param {Object<string, NodeInfo>} node_list
316
+ * @returns {Object<string, NodeInfo>}
317
+ */
318
+ copyNodeList(node_list) {
319
+ /** @type {Object<string, NodeInfo>} */
320
+ const copy_node_list = {};
321
+ Object.entries(node_list).forEach(([path, node]) => {
322
+ copy_node_list[path] = {...node};
323
+ if(node.children) {
324
+ copy_node_list[path].children = this.copyNodeList(node.children);
219
325
  }
220
326
  });
327
+ return copy_node_list;
221
328
  }
222
329
 
223
- // 特殊处理view和response
224
- const node_view = './' + _APP + '/view';
225
- const common_view = './' + cfg_app.common_app + '/view';
226
- if(node_list[node_view] && !node_list[node_view].is_class) {
227
- delete node_list[node_view];
228
- }
229
- if(node_list[common_view] && !node_list[common_view].is_class) {
230
- delete node_list[common_view];
231
- }
232
- const node_response = './' + _APP + '/response';
233
- const common_response = './' + cfg_app.common_app + '/response';
234
- if(node_list[node_response] && !node_list[node_response].is_class) {
235
- delete node_list[node_response];
236
- }
237
- if(node_list[common_response] && !node_list[common_response].is_class) {
238
- delete node_list[common_response];
330
+ /**
331
+ * 处理common库
332
+ * @param {Object<string, NodeInfo>} node_list
333
+ * @param {string} _APP
334
+ */
335
+ calcNodeList(node_list, _APP) {
336
+ // 处理common
337
+ const node_common = './' + cfg_app.common_app;
338
+ if(cfg_app.common_app && node_list[node_common] && node_list[node_common].children) {
339
+ const children = this.copyNodeList(node_list[node_common].children);
340
+ Object.entries(children).forEach(([path, node]) => {
341
+ node_list[path] = node;
342
+ });
343
+ }
344
+
345
+ // 处理app
346
+ const node_app = './' + _APP;
347
+ if(_APP && node_list[node_app] && node_list[node_app].children) {
348
+ Object.entries(node_list[node_app].children).forEach(([path, node]) => {
349
+ const path_common = path.replace(node_app, node_common);
350
+ if(!node_list[path_common]) {
351
+ node_list[path] = node;
352
+ } else if(node_list[path_common].file_type == 'file') {
353
+ delete node_list[path_common];
354
+ node_list[path] = node;
355
+ } else if(node.children) {
356
+ Object.entries(node.children).forEach(([p, n]) => {
357
+ const p_commmon = p.replace(node_app, node_common);
358
+ if (node_list[path_common].children) {
359
+ node_list[path_common].children[p_commmon] && delete node_list[path_common].children[p_commmon];
360
+ node_list[path_common].children[p] = n;
361
+ }
362
+ });
363
+ }
364
+ });
365
+ }
366
+
367
+ // 特殊处理view和response
368
+ const node_view = './' + _APP + '/view';
369
+ const common_view = './' + cfg_app.common_app + '/view';
370
+ if(node_list[node_view] && !node_list[node_view].is_class) {
371
+ delete node_list[node_view];
372
+ }
373
+ if(node_list[common_view] && !node_list[common_view].is_class) {
374
+ delete node_list[common_view];
375
+ }
376
+ const node_response = './' + _APP + '/response';
377
+ const common_response = './' + cfg_app.common_app + '/response';
378
+ if(node_list[node_response] && !node_list[node_response].is_class) {
379
+ delete node_list[node_response];
380
+ }
381
+ if(node_list[common_response] && !node_list[common_response].is_class) {
382
+ delete node_list[common_response];
383
+ }
239
384
  }
240
385
  }
241
386
 
242
- module.exports = start;
387
+ // 创建单例实例
388
+ const generator = new TypesGenerator();
389
+
390
+ module.exports = {
391
+ /**
392
+ * 启动文件监听并自动生成类型定义
393
+ * @returns {fs.FSWatcher|null}
394
+ */
395
+ watch: () => generator.watch(),
396
+
397
+ /**
398
+ * 手动创建类型定义文件
399
+ * @param {string} [f] - 触发变更的文件路径
400
+ * @returns {Promise<void>}
401
+ */
402
+ createFile: (f) => generator.createFile(f),
403
+
404
+ /**
405
+ * 获取生成器实例
406
+ * @returns {TypesGenerator}
407
+ */
408
+ generator
409
+ };