5htp-core 0.0.7 → 0.0.8-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "5htp-core",
3
3
  "description": "5-HTP, scientifically called 5-Hydroxytryptophan, is the precursor of happiness neurotransmitter.",
4
- "version": "0.0.7",
4
+ "version": "0.0.8-2",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -58,7 +58,6 @@
58
58
  "nodemailer": "^6.6.3",
59
59
  "path-to-regexp": "^6.2.0",
60
60
  "picomatch": "^2.3.1",
61
- "preact-render-to-string": "^5.1.19",
62
61
  "react-scrollbars-custom": "^4.0.27",
63
62
  "react-slider": "^2.0.1",
64
63
  "react-textarea-autosize": "^8.3.3",
@@ -73,7 +72,9 @@
73
72
  "uuid-by-string": "^3.0.4",
74
73
  "validator": "^13.7.0",
75
74
  "ws": "^8.2.2",
76
- "yaml": "^1.10.2"
75
+ "yaml": "^1.10.2",
76
+ "preact": "^10.5.15",
77
+ "preact-render-to-string": "^5.1.19"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@types/cookie": "^0.4.1",
@@ -18,7 +18,7 @@
18
18
  font-weight: 600;
19
19
 
20
20
  // Colors
21
- background: #fff;
21
+ background: transparent;
22
22
  color: var(--cTxtBase);
23
23
 
24
24
  &,
@@ -202,7 +202,7 @@ export class ClientContext {
202
202
  public handleError(e: Error) {
203
203
  switch (e.http) {
204
204
  case 401:
205
- this.page?.redirect('/');
205
+ this.page?.go('/');
206
206
  break;
207
207
  default:
208
208
  this.toast.error(e.title || "Uh Oh ...", e.message, null, { autohide: false });
@@ -225,12 +225,13 @@ export class ClientContext {
225
225
  }
226
226
  }
227
227
 
228
+ // Services
228
229
  public modal = createDialog(this, false);
229
230
  public toast = createDialog(this, true);
230
-
231
231
  public socket = new SocketClient(this)
232
232
  public captcha = new Recaptcha(this);
233
233
 
234
+ // Tracking
234
235
  public event( name: string, params?: object ) {
235
236
  if (!window.gtag) return;
236
237
  if (name === 'pageview')
@@ -33,7 +33,8 @@ declare global {
33
33
  interface Window {
34
34
  dev: boolean,
35
35
  context: ClientContext,
36
- user: User
36
+ user: User,
37
+ gtag: (action: string, name: string, params?: any) => void
37
38
  }
38
39
  }
39
40
 
@@ -69,10 +69,10 @@ export default () => {
69
69
 
70
70
  const [pages, setPages] = React.useState<{
71
71
  current: undefined | PageResponse,
72
- previous: undefined | PageResponse
72
+ //previous: undefined | PageResponse
73
73
  }>({
74
74
  current: gui.page,
75
- previous: undefined
75
+ //previous: undefined
76
76
  });
77
77
 
78
78
  const resolvePage = async (request: ClientRequest, locationUpdate?: Update) => {
@@ -116,13 +116,13 @@ export default () => {
116
116
  if (oldPage !== undefined) {
117
117
  setTimeout(() => setPages({
118
118
  current: newpage,
119
- previous: undefined
119
+ //previous: undefined
120
120
  }), 500);
121
121
  }
122
122
 
123
123
  return {
124
124
  current: newpage,
125
- previous: oldPage
125
+ //previous: oldPage
126
126
  }
127
127
  });
128
128
  }
@@ -175,9 +175,9 @@ export default () => {
175
175
 
176
176
  // Render the page component
177
177
  return <>
178
- {pages.previous && (
178
+ {/*pages.previous && (
179
179
  <Page page={pages.previous} key={pages.previous.id === undefined ? undefined : 'page_' + pages.previous.id} />
180
- )}
180
+ )*/}
181
181
 
182
182
  {pages.current && (
183
183
  <Page page={pages.current} isCurrent key={pages.current.id === undefined ? undefined : 'page_' + pages.current.id} />
@@ -15,7 +15,7 @@ import ConfigParser, { TEnvConfig } from './config';
15
15
  ----------------------------------*/
16
16
 
17
17
  type THookName = 'ready' | 'cleanup' | 'error'
18
- type THook = () => void;
18
+ type THook = () => Promise<void>;
19
19
 
20
20
  type TServiceOptions = {
21
21
  instanciate: boolean
@@ -91,6 +91,20 @@ export class App {
91
91
  ----------------------------------*/
92
92
 
93
93
  public constructor() {
94
+
95
+ // Gestion crash
96
+ process.on('unhandledRejection', (error: any, promise: any) => {
97
+
98
+ console.error("Unhandled promise rejection:", error);
99
+
100
+ // Send email report
101
+ if (this.isLoaded('console'))
102
+ $.console.bugReport.server(error);
103
+ else
104
+ console.error(`Unable to send bug report: console service not loaded.`);
105
+
106
+ });
107
+
94
108
  // Load config files
95
109
  const configParser = new ConfigParser( this.path.root );
96
110
  this.env = configParser.env();
@@ -106,21 +120,47 @@ export class App {
106
120
  console.log("Configure services with", this.config);
107
121
  }
108
122
 
109
- public register( id: string, Service: TServiceClass, options: Partial<TServiceOptions> = {}) {
123
+ // Register a service
124
+ public register<TServiceName extends keyof Core.Services>(
125
+ id: TServiceName,
126
+ Service: TServiceClass,
127
+ options: Partial<TServiceOptions> = {}
128
+ ) {
110
129
 
111
130
  // Pas d'export default new Service pour chaque fichier de service,
112
131
  // dissuaded'importer ms service sn'importe où, ce qui créé des références circulaires
113
- console.log(`Launching service ${id} ...`, Service);
132
+ console.log(`[services] Registering service ${id} ...`);
114
133
  const service = options.instanciate !== false ? new Service() : Service;
115
- this.services[id] = service;
116
-
117
- // Lorsque service.load est async, une propriété loading doit etre présente
118
- // De façon à ce que les autres services puissent savoir quand ce service est prêt
119
- if ('loading' in service) {
120
- service.loading = service.load();
121
- this.loading.push(service.loading);
122
- } else if ('load' in service)
123
- service.load();
134
+ this.services[id as string] = service;
135
+
136
+ if ('load' in service) {
137
+
138
+ console.log(`[services] Starting service ${id} ...`);
139
+
140
+ // Lorsque service.load est async, une propriété loading doit etre présente
141
+ // De façon à ce que les autres services puissent savoir quand ce service est prêt
142
+ if ('loading' in service) {
143
+
144
+ console.log(`[services] Waiting service ${id} to be fully loaded ...`);
145
+ service.loading = service.load().then(() => {
146
+ console.info(`[service] Service ${id} successfully started.`);
147
+ }).catch(e => {
148
+ // Bug report via email
149
+ console.error(`[service] Error while starting the ${id} service:`, e);
150
+ e.message = `Start ${id} service: ` + e.message;
151
+ $.console.bugReport.server(e);
152
+ });;
153
+
154
+ this.loading.push(service.loading);
155
+
156
+ } else
157
+ service.load();
158
+ }
159
+ }
160
+
161
+ // Test if a service was registered
162
+ public isLoaded( id: keyof Core.Services ) {
163
+ return id in this.services;
124
164
  }
125
165
 
126
166
  public on( name: THookName, callback: THook ) {
@@ -128,23 +168,36 @@ export class App {
128
168
  return this;
129
169
  }
130
170
 
171
+ public runHook( hookName: THookName ) {
172
+ console.info(`[hook] Run all ${hookName} hook (${this.hooks.ready.length}).`);
173
+ return Promise.all(
174
+ this.hooks.ready.map(
175
+ cb => cb().catch(e => {
176
+ console.error(`[hook] Error while executing hook ${hookName}:`, e);
177
+ })
178
+ )
179
+ ).then(() => {
180
+ console.info(`[hook] Hooks ${hookName} executed with success.`);
181
+ })
182
+ }
183
+
131
184
  /*----------------------------------
132
185
  - LAUNCH
133
186
  ----------------------------------*/
134
187
  public async launch() {
135
188
 
136
- console.info(`Waiting for services to be ready ...`);
189
+ console.info(`[boot] Waiting for all services to be ready ...`);
137
190
  await Promise.all( this.loading );
138
191
 
139
- console.info(`Launching application ...`);
140
- await Promise.all( this.hooks.ready.map(cb => cb()) );
192
+ console.info(`[boot] Launching application ...`);
193
+ await this.runHook('ready');
141
194
 
142
195
  // NOTE: Useless ?
143
196
  /*if (this.hmr)
144
197
  this.activateHMR();*/
145
198
 
146
- console.info(`Application is ready.`);
147
-
199
+ console.info(`[boot] Application is ready.`);
200
+
148
201
  this.launched = true;
149
202
 
150
203
  }
@@ -2,68 +2,123 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
+ // Node
6
+ import path from 'path';
7
+
5
8
  // Npm
6
9
  import hInterval from 'human-interval';
10
+ import fs from 'fs-extra';
11
+
12
+ // Core
13
+ import app from '@server/app';
7
14
 
8
15
  // Libs
9
- import redis from '@server/services/redis';
10
- import { ErreurCritique } from '@common/errors';
16
+
17
+ /*----------------------------------
18
+ - TYPES
19
+ ----------------------------------*/
20
+
21
+ type TPrimitiveValue = string | boolean | number | undefined | {[key: string]: TPrimitiveValue} | TPrimitiveValue[];
22
+
23
+ type TExpirationDelay = string | number | Date | null;
24
+
25
+ type CacheEntry = {
26
+ // Value
27
+ value: TPrimitiveValue,
28
+ // Expiration Timestamp
29
+ expiration?: number
30
+ };
11
31
 
12
32
  /*----------------------------------
13
33
  - SERVICE
14
34
  ----------------------------------*/
15
- const Cache = {
35
+ class Cache {
36
+
37
+ private cacheFile = path.join(app.path.data, 'cache/mem.json');
38
+
39
+ private data: {[key: string]: CacheEntry | undefined} = {};
40
+
41
+ private changes: number = 0;
42
+
43
+ public constructor() {
44
+
45
+ setInterval(() => this.cleanMem(), 10000);
46
+
47
+ if (fs.existsSync(this.cacheFile))
48
+ this.data = fs.readJSONSync(this.cacheFile)
49
+ }
50
+
51
+ private cleanMem() {
52
+
53
+ console.log("[cache] Clean memory");
54
+
55
+ const now = Date.now();
56
+ for (const key in this.data) {
57
+ const entry = this.data[ key ];
58
+ if (entry?.expiration && entry.expiration < now)
59
+ this.del(key);
60
+ }
61
+
62
+ if (this.changes > 0)
63
+ fs.outputJSONSync(this.cacheFile, this.data);
64
+ }
16
65
 
17
66
  // Expiration = Durée de vie en secondes ou date max
18
67
  // Retourne null quand pas de valeur
19
- async get<TValeur>(
68
+ public get<TValeur extends TPrimitiveValue>(
69
+ cle: string,
70
+ func?: (() => Promise<TValeur>),
71
+ expiration?: TExpirationDelay,
72
+ avecDetails?: true
73
+ ): Promise<CacheEntry>;
74
+
75
+ public get<TValeur extends TPrimitiveValue>(
76
+ cle: string,
77
+ func: (() => Promise<TValeur>),
78
+ expiration?: TExpirationDelay,
79
+ avecDetails?: false
80
+ ): Promise<null | TValeur>;
81
+
82
+ public async get<TValeur extends TPrimitiveValue>(
20
83
  cle: string,
21
- func: Function | null = null,
22
- expiration: number | Date | null = null,
84
+ func?: (() => Promise<TValeur>),
85
+ expiration?: TExpirationDelay,
23
86
  avecDetails?: boolean
24
- ): Promise<null | TValeur> {
87
+ ): Promise<null | TValeur | CacheEntry> {
88
+
89
+ let retour: CacheEntry | undefined = this.data[cle];
90
+
91
+ console.log(`[cache] Get "${cle}".`);
25
92
 
26
- let retour: any = await this.getVal(cle);
93
+ // Expired
94
+ if (retour?.expiration && retour.expiration < Date.now()){
95
+ console.log(`[cache] Key ${cle} expired.`);
96
+ retour = undefined;
97
+ }
27
98
 
28
99
  // Donnée inexistante
29
- if (retour === null && func !== null) {
100
+ if (retour === undefined && func !== undefined) {
30
101
 
31
102
  // Rechargement
32
- retour = await func();
103
+ retour = {
104
+ value: await func(),
105
+ expiration: expiration
106
+ ? this.delayToTimestamp(expiration)
107
+ : undefined
108
+ }
33
109
 
34
110
  // undefined retourné = pas d'enregistrement
35
- if (retour !== undefined)
111
+ if (retour.value !== undefined)
36
112
  await this.set(cle, retour, expiration);
37
113
  }
38
114
 
39
- return avecDetails
40
- ? {
41
- donnees: retour
42
- }
43
- : retour;
44
- },
45
-
46
- getVal<TValeur>(cle: string): Promise<TValeur | null> {
47
- return new Promise((resolve) => {
48
-
49
- redis.instance.get(cle, (err, val) => {
115
+ if (retour === undefined)
116
+ return null;
50
117
 
51
- if (val === null) {
52
- resolve( null );
53
- } else {
54
- try {
55
- resolve( JSON.parse(val) )
56
- } catch (error) {
57
-
58
- console.warn(`Error while parsing JSON value from cache (id: ${cle})`, error, 'Raw value:', val);
59
- resolve(null);
60
-
61
- }
62
- }
63
-
64
- });
65
- });
66
- },
118
+ return avecDetails
119
+ ? retour
120
+ : retour.value as TValeur;
121
+ };
67
122
 
68
123
  /**
69
124
  * Put in cache a JSON value, associated with an unique ID.
@@ -76,49 +131,44 @@ const Cache = {
76
131
  * - null: no expiration (default)
77
132
  * @returns A void promise
78
133
  */
79
- set( cle: string, val: any, expiration: string | number | Date | null = null ): Promise<void> {
80
- console.log("Updating cache " + cle);
81
- return new Promise((resolve) => {
82
-
83
- //console.info('Enregistrement de ' + cle + ' avec la valeur ', val, 'expiration', expiration);
84
-
85
- val = JSON.stringify(val);
134
+ public set( cle: string, val: TPrimitiveValue, expiration: TExpirationDelay = null ): void {
135
+
136
+ console.log("[cache] Updating cache " + cle);
137
+ this.data[ cle ] = {
138
+ value: val,
139
+ expiration: this.delayToTimestamp(expiration)
140
+ }
86
141
 
87
- // Conversion de l'expiration en nombre de secondes (ttl, time to live)
88
- if (expiration === null)
89
- redis.instance.set(cle, val, () => {
90
- resolve()
91
- });
92
- else {
142
+ this.changes++;
143
+ };
93
144
 
94
- let secondes: number;
145
+ public del( cle: string ): void {
146
+ this.data[ cle ] = undefined;
147
+ this.changes++;
148
+ }
95
149
 
96
- // H expression
97
- if (typeof expiration === 'string') {
98
150
 
99
- const ms = hInterval(expiration);
100
- if (ms === undefined) throw new Error(`Invalid period string: ` + expiration);
101
- secondes = ms / 1000;
151
+ /*----------------------------------
152
+ - UTILS
153
+ ----------------------------------*/
154
+ private delayToTimestamp( delay: TExpirationDelay ): number {
102
155
 
103
- // Via durée de vie en secondes
104
- } else if (typeof expiration === 'number')
105
- secondes = expiration;
106
- // Date limite
107
- else
108
- secondes = (expiration.getTime() - (new Date).getTime()) / 1000;
156
+ // H expression
157
+ if (typeof delay === 'string') {
109
158
 
110
- redis.instance.set(cle, val, 'EX', secondes, () => {
111
- resolve()
112
- });
113
- }
114
- });
115
- },
159
+ const ms = hInterval(delay);
160
+ if (ms === undefined) throw new Error(`Invalid period string: ` + delay);
161
+ return Date.now() + ms;
116
162
 
117
- del( cle: string ): Promise<void> {
118
- return new Promise((resolve) => {
119
- redis.instance.del(cle, () => resolve());
120
- });
163
+ // Via durée de vie en secondes
164
+ } else if (typeof delay === 'number')
165
+ return Date.now() + delay;
166
+ // Date limite
167
+ else if (delay !== null)
168
+ return delay.getTime();
169
+ else
170
+ return Date.now();
121
171
  }
122
172
  }
123
173
 
124
- export default Cache;
174
+ export default new Cache;
@@ -4,17 +4,6 @@ moduleAlias.addAliases({
4
4
  'react-dom': "preact/compat",
5
5
  })
6
6
 
7
- /*----------------------------------
8
- - DEBUG
9
- ----------------------------------*/
10
-
11
- // Gestion crash
12
- process.on('unhandledRejection', (error: any, promise: any) => {
13
-
14
- console.error("Unhandled promise rejection:", error);
15
-
16
- });
17
-
18
7
  /*----------------------------------
19
8
  - DATES & TIMZEONE
20
9
  ----------------------------------*/
@@ -70,7 +70,7 @@ export default class BugReporter {
70
70
  public async server( error: Error, request?: ServerRequest ) {
71
71
 
72
72
  // error should be printed in the console, so they're acccessible from logs
73
- console.error(error);
73
+ console.error(`Sending bug report for the following error:`, error);
74
74
 
75
75
  // Prevent duplicates
76
76
  if (!this.shouldSendReport('server', request?.user?.name, undefined, error.message))
@@ -79,7 +79,6 @@ export default class BugReporter {
79
79
  // Get context
80
80
  const now = new Date();
81
81
  const hash = uuid();
82
- const erroTitle = "Server Bug: " + error.message;
83
82
  const { channelType, channelId } = this.console.getChannel();
84
83
 
85
84
  // On envoi l'email avant l'insertion dans bla bdd
@@ -90,31 +89,36 @@ export default class BugReporter {
90
89
  );
91
90
 
92
91
  // Send notification
93
- $.email.send({
94
- to: app.identity.author.email,
95
- subject: "Bug serveur: " + erroTitle,
96
- html: `
97
- <a href="${app.env.url}/admin/activity/requests/${channelId}">
98
- View Request details & console
99
- </a>
100
- <br/>
101
- ${logsHtml}
102
- `
103
- });
104
- // Memorize
105
- $.sql.insert('BugServer', {
106
- // Context
107
- hash: hash,
108
- date: now,
109
- channelType,
110
- channelId,
111
- // User
112
- user: request?.user?.name,
113
- ip: request?.ip,
114
- // Error
115
- stacktrace: error.stack || error.message,
116
- logs: logsHtml
117
- });
92
+ if (app.isLoaded('email'))
93
+ $.email.send({
94
+ to: app.identity.author.email,
95
+ subject: "Server bug: " + error.message,
96
+ html: `
97
+ <a href="${app.env.url}/admin/activity/requests/${channelId}">
98
+ View Request details & console
99
+ </a>
100
+ <br/>
101
+ ${logsHtml}
102
+ `
103
+ });
104
+ else
105
+ console.error("Unable to send bug report: email service not loaded.");
106
+
107
+ /*if (app.isLoaded('sql'))
108
+ // Memorize
109
+ $.sql.insert('BugServer', {
110
+ // Context
111
+ hash: hash,
112
+ date: now,
113
+ channelType,
114
+ channelId,
115
+ // User
116
+ user: request?.user?.name,
117
+ ip: request?.ip,
118
+ // Error
119
+ stacktrace: error.stack || error.message,
120
+ logs: logsHtml
121
+ });*/
118
122
 
119
123
  // Update error message
120
124
  error.message = "A bug report has been sent to my personal mailbox. Sorry for the inconvenience.";
File without changes
@@ -256,7 +256,7 @@ export class Console {
256
256
  let html = logs.map( log => logToHTML( log, this )).join('\n');
257
257
 
258
258
  if (full) {
259
- const consoleCss = `background: #000; padding: 20px; font-family: 'monospace'; font-size: 12px; line-height: 20px;`
259
+ const consoleCss = `background: #000; padding: 20px; font-family: 'Fira Mono', 'monospace', 'Monaco'; font-size: 12px; line-height: 20px;`
260
260
  html = '<div style="' + consoleCss + '">' + html + '</div>';
261
261
  }
262
262
 
@@ -49,7 +49,7 @@ export class WebSocketCommander {
49
49
  public scopes: {[path: string]: SocketScope} = {}
50
50
 
51
51
  public constructor() {
52
- app.on('cleanup', () => {
52
+ app.on('cleanup', async () => {
53
53
  this.closeAll();
54
54
  });
55
55
  }