proteum 1.0.2 → 2.0.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/AGENTS.md +101 -0
- package/agents/codex/AGENTS.md +95 -0
- package/agents/codex/CODING_STYLE.md +71 -0
- package/agents/codex/agents.md.zip +0 -0
- package/agents/codex/client/AGENTS.md +102 -0
- package/agents/codex/client/pages/AGENTS.md +35 -0
- package/agents/codex/server/routes/AGENTS.md +12 -0
- package/agents/codex/server/services/AGENTS.md +137 -0
- package/agents/codex/tests/AGENTS.md +8 -0
- package/cli/app/config.ts +13 -11
- package/cli/app/index.ts +74 -82
- package/cli/bin.js +1 -1
- package/cli/commands/build.ts +51 -14
- package/cli/commands/check.ts +19 -0
- package/cli/commands/deploy/app.ts +4 -8
- package/cli/commands/deploy/web.ts +16 -20
- package/cli/commands/dev.ts +189 -64
- package/cli/commands/devEvents.ts +106 -0
- package/cli/commands/init.ts +63 -57
- package/cli/commands/lint.ts +21 -0
- package/cli/commands/refresh.ts +18 -0
- package/cli/commands/typecheck.ts +18 -0
- package/cli/compiler/client/identite.ts +80 -53
- package/cli/compiler/client/index.ts +139 -213
- package/cli/compiler/common/bundleAnalysis.ts +94 -0
- package/cli/compiler/common/clientManifest.ts +67 -0
- package/cli/compiler/common/controllers.ts +288 -0
- package/cli/compiler/common/files/autres.ts +7 -18
- package/cli/compiler/common/files/images.ts +40 -37
- package/cli/compiler/common/files/style.ts +11 -22
- package/cli/compiler/common/generatedRouteModules.ts +368 -0
- package/cli/compiler/common/index.ts +31 -65
- package/cli/compiler/common/loaders/forbid-ssr-import.js +13 -0
- package/cli/compiler/common/rspackAliases.ts +13 -0
- package/cli/compiler/common/scripts.ts +37 -0
- package/cli/compiler/index.ts +781 -230
- package/cli/compiler/server/index.ts +59 -75
- package/cli/compiler/writeIfChanged.ts +21 -0
- package/cli/index.ts +71 -72
- package/cli/paths.ts +51 -57
- package/cli/print.ts +17 -11
- package/cli/tsconfig.json +5 -4
- package/cli/utils/agents.ts +100 -0
- package/cli/utils/check.ts +71 -0
- package/cli/utils/index.ts +1 -3
- package/cli/utils/keyboard.ts +8 -25
- package/cli/utils/runProcess.ts +30 -0
- package/client/app/component.tsx +29 -29
- package/client/app/index.ts +36 -57
- package/client/app/service.ts +7 -12
- package/client/app.tsconfig.json +2 -2
- package/client/components/Dialog/Manager.ssr.tsx +40 -0
- package/client/components/Dialog/Manager.tsx +119 -150
- package/client/components/Dialog/status.tsx +3 -3
- package/client/components/index.ts +1 -1
- package/client/components/types.d.ts +1 -3
- package/client/dev/hmr.ts +65 -0
- package/client/global.d.ts +2 -2
- package/client/hooks.ts +6 -9
- package/client/index.ts +2 -1
- package/client/islands/index.ts +7 -0
- package/client/islands/useDeferredModule.ts +199 -0
- package/client/pages/_layout/index.tsx +4 -12
- package/client/pages/useHeader.tsx +14 -21
- package/client/router.ts +27 -0
- package/client/services/router/components/Link.tsx +34 -27
- package/client/services/router/components/Page.tsx +6 -14
- package/client/services/router/components/router.ssr.tsx +36 -0
- package/client/services/router/components/router.tsx +63 -83
- package/client/services/router/index.tsx +185 -220
- package/client/services/router/request/api.ts +97 -119
- package/client/services/router/request/history.ts +2 -2
- package/client/services/router/request/index.ts +13 -12
- package/client/services/router/request/multipart.ts +72 -62
- package/client/services/router/response/index.tsx +68 -61
- package/client/services/router/response/page.ts +28 -32
- package/client/utils/dom.ts +17 -33
- package/common/app/index.ts +3 -3
- package/common/data/chaines/index.ts +22 -23
- package/common/data/dates.ts +35 -70
- package/common/data/markdown.ts +42 -39
- package/common/dev/serverHotReload.ts +26 -0
- package/common/errors/index.tsx +110 -142
- package/common/router/contracts.ts +29 -0
- package/common/router/index.ts +89 -108
- package/common/router/layouts.ts +34 -47
- package/common/router/pageSetup.ts +50 -0
- package/common/router/register.ts +53 -24
- package/common/router/request/api.ts +30 -36
- package/common/router/request/index.ts +2 -8
- package/common/router/response/index.ts +8 -15
- package/common/router/response/page.ts +70 -58
- package/common/utils.ts +1 -1
- package/doc/TODO.md +1 -1
- package/eslint.js +62 -0
- package/package.json +12 -47
- package/prettier.config.cjs +9 -0
- package/scripts/cleanup-generated-controllers.ts +62 -0
- package/scripts/fix-reference-app-typing.ts +490 -0
- package/scripts/refactor-client-app-imports.ts +244 -0
- package/scripts/refactor-client-pages.ts +587 -0
- package/scripts/refactor-server-controllers.ts +470 -0
- package/scripts/refactor-server-runtime-aliases.ts +360 -0
- package/scripts/restore-client-app-import-files.ts +41 -0
- package/scripts/restore-files-from-git-head.ts +20 -0
- package/scripts/update-codex-agents.ts +35 -0
- package/server/app/commands.ts +35 -64
- package/server/app/container/config.ts +48 -59
- package/server/app/container/console/index.ts +202 -248
- package/server/app/container/index.ts +33 -71
- package/server/app/controller/index.ts +61 -0
- package/server/app/index.ts +39 -105
- package/server/app/service/container.ts +41 -42
- package/server/app/service/index.ts +120 -147
- package/server/context.ts +1 -1
- package/server/index.ts +25 -1
- package/server/services/auth/index.ts +75 -115
- package/server/services/auth/router/index.ts +31 -32
- package/server/services/auth/router/request.ts +14 -16
- package/server/services/cron/CronTask.ts +13 -26
- package/server/services/cron/index.ts +14 -36
- package/server/services/disks/driver.ts +40 -58
- package/server/services/disks/drivers/local/index.ts +79 -90
- package/server/services/disks/drivers/s3/index.ts +116 -163
- package/server/services/disks/index.ts +23 -38
- package/server/services/email/index.ts +45 -104
- package/server/services/email/utils.ts +14 -27
- package/server/services/fetch/index.ts +53 -85
- package/server/services/prisma/Facet.ts +39 -91
- package/server/services/prisma/index.ts +74 -110
- package/server/services/router/generatedRuntime.ts +29 -0
- package/server/services/router/http/index.ts +78 -73
- package/server/services/router/http/multipart.ts +19 -42
- package/server/services/router/index.ts +378 -365
- package/server/services/router/request/api.ts +26 -25
- package/server/services/router/request/index.ts +44 -51
- package/server/services/router/request/service.ts +7 -11
- package/server/services/router/request/validation/zod.ts +111 -148
- package/server/services/router/response/index.ts +110 -125
- package/server/services/router/response/mask/Filter.ts +31 -72
- package/server/services/router/response/mask/index.ts +8 -15
- package/server/services/router/response/mask/selecteurs.ts +11 -25
- package/server/services/router/response/page/clientManifest.ts +25 -0
- package/server/services/router/response/page/document.tsx +199 -127
- package/server/services/router/response/page/index.tsx +89 -94
- package/server/services/router/service.ts +13 -15
- package/server/services/schema/index.ts +17 -26
- package/server/services/schema/request.ts +19 -33
- package/server/services/schema/router/index.ts +8 -11
- package/server/services/security/encrypt/aes/index.ts +15 -35
- package/server/utils/slug.ts +29 -35
- package/skills/clean-project-code/SKILL.md +63 -0
- package/skills/clean-project-code/agents/openai.yaml +4 -0
- package/tsconfig.common.json +4 -3
- package/tsconfig.json +4 -1
- package/types/aliases.d.ts +17 -21
- package/types/controller-input.test.ts +48 -0
- package/types/express-extra.d.ts +6 -0
- package/types/global/constants.d.ts +13 -0
- package/types/global/express-extra.d.ts +6 -0
- package/types/global/modules.d.ts +13 -16
- package/types/global/utils.d.ts +17 -49
- package/types/global/vendors.d.ts +62 -0
- package/types/icons.d.ts +65 -1
- package/types/uuid.d.ts +3 -0
- package/types/vendors.d.ts +62 -0
- package/cli/compiler/common/babel/index.ts +0 -170
- package/cli/compiler/common/babel/plugins/index.ts +0 -0
- package/cli/compiler/common/babel/plugins/services.ts +0 -586
- package/cli/compiler/common/babel/routes/imports.ts +0 -127
- package/cli/compiler/common/babel/routes/routes.ts +0 -1130
- package/client/services/captcha/index.ts +0 -67
- package/client/services/socket/index.ts +0 -147
- package/common/data/rte/nodes.ts +0 -83
- package/common/data/stats.ts +0 -90
- package/common/utils/rte.ts +0 -183
- package/server/services/auth/old.ts +0 -277
- package/server/services/cache/commands.ts +0 -41
- package/server/services/cache/index.ts +0 -297
- package/server/services/cache/service.json +0 -6
- package/server/services/socket/index.ts +0 -162
- package/server/services/socket/scope.ts +0 -226
- package/server/services/socket/service.json +0 -6
- package/server/services_old/SocketClient.ts +0 -92
- package/server/services_old/Token.old.ts +0 -97
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import md5 from 'md5';
|
|
3
|
-
import { OAuth2Client, LoginTicket } from 'google-auth-library';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type AuthResponse = {
|
|
8
|
-
token: string,
|
|
9
|
-
redirect: string,
|
|
10
|
-
user: User
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export default class {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
protected async ready() {
|
|
17
|
-
|
|
18
|
-
// Google auth client
|
|
19
|
-
if (this.config.google) {
|
|
20
|
-
|
|
21
|
-
const httpConfig = this.app.http.publicUrl;
|
|
22
|
-
|
|
23
|
-
this.googleClient = new OAuth2Client(
|
|
24
|
-
this.config.google.web.clientId, // Google Client ID
|
|
25
|
-
this.config.google.web.secret, // Private key
|
|
26
|
-
httpConfig + "/auth/google/response" // Redirect url
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
private googleClient: OAuth2Client | undefined;
|
|
33
|
-
|
|
34
|
-
public async FromGoogle(request: ServerRequest): Promise<string> {
|
|
35
|
-
|
|
36
|
-
if (!this.googleClient)
|
|
37
|
-
throw new Forbidden(`Authentication method disabled.`);
|
|
38
|
-
|
|
39
|
-
// Register start time, so we can determine the signup time to display
|
|
40
|
-
request.response?.cookie('signupstart', Date.now());
|
|
41
|
-
|
|
42
|
-
// Google auth doesn't support local env
|
|
43
|
-
// So we simulate that google sent a response with the user email
|
|
44
|
-
if (app.env.name === 'local') {
|
|
45
|
-
const { redirect } = await this.Auth("tbsoftwares23@gmail.com", request, true);
|
|
46
|
-
return redirect;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return this.googleClient.generateAuthUrl({
|
|
50
|
-
access_type: 'offline',
|
|
51
|
-
scope: [
|
|
52
|
-
"email", "profile"
|
|
53
|
-
]
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
public async GoogleResponse(
|
|
59
|
-
type: 'code' | 'token',
|
|
60
|
-
codeOrToken: string | undefined,
|
|
61
|
-
request: ServerRequest,
|
|
62
|
-
): Promise<AuthResponse> {
|
|
63
|
-
|
|
64
|
-
const googleConfig = this.config.google;
|
|
65
|
-
if (!this.googleClient || !googleConfig)
|
|
66
|
-
throw new Forbidden(`Authentication method disabled.`);
|
|
67
|
-
|
|
68
|
-
if (codeOrToken === undefined)
|
|
69
|
-
throw new Forbidden("Bad code / token");
|
|
70
|
-
|
|
71
|
-
if (type === 'code') {
|
|
72
|
-
const r = await this.googleClient.getToken(codeOrToken);
|
|
73
|
-
return this.GoogleResponse('token', r.tokens.id_token, request);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
this.config.debug && console.log(LogPrefix, "Auth via google", googleConfig);
|
|
77
|
-
|
|
78
|
-
let ticket: LoginTicket;
|
|
79
|
-
try {
|
|
80
|
-
ticket = await this.googleClient.verifyIdToken({
|
|
81
|
-
idToken: codeOrToken,
|
|
82
|
-
audience: [
|
|
83
|
-
googleConfig.web.clientId,
|
|
84
|
-
googleConfig.android.clientId,
|
|
85
|
-
]
|
|
86
|
-
});
|
|
87
|
-
} catch (error) {
|
|
88
|
-
throw new Forbidden(`Google denied your login attempt: ` + error.message + `. If you don't think it's normal, please contact us.`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const payload = ticket.getPayload();
|
|
92
|
-
if (payload === undefined)
|
|
93
|
-
throw new Forbidden("Invalid payload");
|
|
94
|
-
const { email, sub: google_id } = payload;
|
|
95
|
-
|
|
96
|
-
if (email === undefined)
|
|
97
|
-
throw new Forbidden("Unable to get your email address from the Google sign-in.");
|
|
98
|
-
|
|
99
|
-
return await this.Auth(email, request, true);
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Login u
|
|
105
|
-
* @param email Email that identifies the account to log in
|
|
106
|
-
* @param request Used to call security-related features
|
|
107
|
-
* @param canPass true when from 3rd party auth, we're sure the email is owned bu the current user
|
|
108
|
-
* @param userInfo User field to update (eg: lastLogin)
|
|
109
|
-
* @returns
|
|
110
|
-
*/
|
|
111
|
-
public async Auth(
|
|
112
|
-
email: string,
|
|
113
|
-
request: ServerRequest,
|
|
114
|
-
canPass: boolean = false,
|
|
115
|
-
userInfo: Partial<User> = {}
|
|
116
|
-
): Promise<AuthResponse> {
|
|
117
|
-
|
|
118
|
-
let user = await this.getData('email = ' + this.sql.esc(email));
|
|
119
|
-
let ip: IP;
|
|
120
|
-
let redirect: string;
|
|
121
|
-
if (!user) { // Signup
|
|
122
|
-
|
|
123
|
-
ip = await request.detect.botsAndMultiaccount();
|
|
124
|
-
|
|
125
|
-
// Create user
|
|
126
|
-
({ user, redirect } = await this.Signup(email, request));
|
|
127
|
-
|
|
128
|
-
} else if (!canPass) { // Send login email
|
|
129
|
-
|
|
130
|
-
throw new Forbidden("This option is not available");
|
|
131
|
-
|
|
132
|
-
// If the current IP was used to connect to another account that the current
|
|
133
|
-
ip = await request.detect.botsAndMultiaccount(user.email);
|
|
134
|
-
|
|
135
|
-
// Send email with link to /auth/token (expiration 5 min)
|
|
136
|
-
|
|
137
|
-
// Route /auth/:token: check si même ip, même device
|
|
138
|
-
|
|
139
|
-
} else { // Login
|
|
140
|
-
|
|
141
|
-
redirect = config.logoutUrl;
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/*await this.sql`
|
|
146
|
-
INSERT INTO UserLogin SET
|
|
147
|
-
user = ${user.email},
|
|
148
|
-
date = NOW(),
|
|
149
|
-
ip = ${request.ip},
|
|
150
|
-
device = ${request.deviceString()}
|
|
151
|
-
`.run();*/
|
|
152
|
-
|
|
153
|
-
const token = request.auth.login(user.email);
|
|
154
|
-
|
|
155
|
-
return { token, user, redirect };
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
public async Signup(email: string, request: ServerRequest, moreInfos: Partial<User> = {}) {
|
|
160
|
-
|
|
161
|
-
let username = email.split('@')[0];
|
|
162
|
-
|
|
163
|
-
// Prefix username if alreasy existing
|
|
164
|
-
const duplicates = await this.sql`
|
|
165
|
-
FROM User
|
|
166
|
-
WHERE name REGEXP CONCAT('^', ${username}, '[0-9]*$');
|
|
167
|
-
`.count();
|
|
168
|
-
|
|
169
|
-
if (duplicates !== 0)
|
|
170
|
-
username += duplicates;
|
|
171
|
-
|
|
172
|
-
const user: Partial<User> = {
|
|
173
|
-
email,
|
|
174
|
-
emailHash: md5(email),
|
|
175
|
-
name: username,
|
|
176
|
-
referrer: request.cookies.r,
|
|
177
|
-
utm: request.cookies.utm,
|
|
178
|
-
...moreInfos
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Hook
|
|
182
|
-
if (this.beforeSignup !== undefined)
|
|
183
|
-
await this.beforeSignup( user );
|
|
184
|
-
|
|
185
|
-
// Referrer
|
|
186
|
-
if (user.referrer !== undefined) {
|
|
187
|
-
|
|
188
|
-
const refExists = await this.sql`FROM User WHERE name = ${user.referrer}`.exists();
|
|
189
|
-
if (!refExists)
|
|
190
|
-
user.referrer = undefined;
|
|
191
|
-
else {
|
|
192
|
-
|
|
193
|
-
await this.sql.upsert('UserStats', {
|
|
194
|
-
user: user.referrer,
|
|
195
|
-
date: new Date,
|
|
196
|
-
refSignups: 1
|
|
197
|
-
}, ['refSignups'], {
|
|
198
|
-
upsertMode: 'increment'
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Create user
|
|
204
|
-
await this.sql.insert('User', user);
|
|
205
|
-
await this.sql.update('logs.IP', { user_name: username }, { address: request.ip });
|
|
206
|
-
|
|
207
|
-
// Hook
|
|
208
|
-
let redirect: string = '/';
|
|
209
|
-
if (this.afterSignup !== undefined)
|
|
210
|
-
({ redirect } = await this.afterSignup(user));
|
|
211
|
-
|
|
212
|
-
// TODO: download user avatar from gravatar
|
|
213
|
-
// remove user.emailHash
|
|
214
|
-
|
|
215
|
-
// Notif email
|
|
216
|
-
await this.email.send({
|
|
217
|
-
to: app.identity.author.email,
|
|
218
|
-
subject: app.identity.name + ": New User",
|
|
219
|
-
html: JSON.stringify(user)
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// TODO
|
|
223
|
-
// this.hook.signup(user);
|
|
224
|
-
//Achievements.activity(user, 'just signed up', null);
|
|
225
|
-
|
|
226
|
-
// Instanciation
|
|
227
|
-
return { user, redirect };
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
public async setReferrer( referrer: unknown, { user, response, cookies, req, detect, request }: ServerRequest) {
|
|
232
|
-
|
|
233
|
-
if (!response || user)
|
|
234
|
-
return;
|
|
235
|
-
|
|
236
|
-
// Source tracking
|
|
237
|
-
if (!cookies.utm) {
|
|
238
|
-
|
|
239
|
-
const data = req.query;
|
|
240
|
-
|
|
241
|
-
const utm = [data.utm_medium, data.utm_source, data.utm_campaign]
|
|
242
|
-
.filter(a => a !== undefined);
|
|
243
|
-
|
|
244
|
-
response.cookie('utm', utm.join(','));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Referral program
|
|
248
|
-
if (cookies.r === undefined && typeof referrer === "string") {
|
|
249
|
-
|
|
250
|
-
const conflict = await detect.conflictOfInterest( referrer, request );
|
|
251
|
-
if (conflict)
|
|
252
|
-
return;
|
|
253
|
-
|
|
254
|
-
// Check if user exists
|
|
255
|
-
const referrerExists = await this.sql`FROM User WHERE name = ${referrer}`.count();
|
|
256
|
-
if (referrerExists === 0)
|
|
257
|
-
return;
|
|
258
|
-
|
|
259
|
-
// await secutity.detectCheatAttempts();
|
|
260
|
-
// => Check IP (reprendre code existant)
|
|
261
|
-
// => Check si IP pas déjà utilisée par un autre membre (1 compte par IP)
|
|
262
|
-
|
|
263
|
-
// Tracking cookie for signup
|
|
264
|
-
response.cookie('r', referrer);
|
|
265
|
-
|
|
266
|
-
// Count the clic
|
|
267
|
-
await this.sql.upsert('UserStats', {
|
|
268
|
-
user: referrer,
|
|
269
|
-
date: new Date,
|
|
270
|
-
refClics: 1
|
|
271
|
-
}, ['refClics'], {
|
|
272
|
-
upsertMode: 'increment'
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// Npm
|
|
6
|
-
import sizeOf from 'object-sizeof';
|
|
7
|
-
|
|
8
|
-
// Core
|
|
9
|
-
|
|
10
|
-
// Specific
|
|
11
|
-
import type CacheManager from '.';
|
|
12
|
-
|
|
13
|
-
/*----------------------------------
|
|
14
|
-
- TYPES
|
|
15
|
-
----------------------------------*/
|
|
16
|
-
|
|
17
|
-
/*----------------------------------
|
|
18
|
-
- SERVICE
|
|
19
|
-
----------------------------------*/
|
|
20
|
-
export default (cache: CacheManager, app = cache.app) => [
|
|
21
|
-
|
|
22
|
-
app.command('cache', 'Manage the cache service', [
|
|
23
|
-
|
|
24
|
-
app.command('list', 'List cache entries', async () => {
|
|
25
|
-
|
|
26
|
-
return Object.entries(cache.data).map(([ key, entry ]) => ({
|
|
27
|
-
key,
|
|
28
|
-
type: typeof entry?.value,
|
|
29
|
-
size: sizeOf(entry?.value),
|
|
30
|
-
expires: entry?.expiration || 'No expiration'
|
|
31
|
-
}))
|
|
32
|
-
}),
|
|
33
|
-
|
|
34
|
-
app.command('delete', 'List cache entries', async (key?: string) => {
|
|
35
|
-
|
|
36
|
-
await cache.del(key);
|
|
37
|
-
|
|
38
|
-
return true;
|
|
39
|
-
})
|
|
40
|
-
])
|
|
41
|
-
]
|
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// Node
|
|
6
|
-
import path from 'path';
|
|
7
|
-
|
|
8
|
-
// Npm
|
|
9
|
-
import hInterval from 'human-interval';
|
|
10
|
-
|
|
11
|
-
// Core
|
|
12
|
-
import type { Application } from '@server/app';
|
|
13
|
-
import Service, { AnyService, TRegisteredService } from '@server/app/service';
|
|
14
|
-
import type { default as DisksManager, Driver } from '../disks';
|
|
15
|
-
|
|
16
|
-
// Specific
|
|
17
|
-
import registerCommands from './commands';
|
|
18
|
-
|
|
19
|
-
/*----------------------------------
|
|
20
|
-
- CONFIG
|
|
21
|
-
----------------------------------*/
|
|
22
|
-
|
|
23
|
-
const LogPrefix = '[cache]';
|
|
24
|
-
|
|
25
|
-
/*----------------------------------
|
|
26
|
-
- TYPES
|
|
27
|
-
----------------------------------*/
|
|
28
|
-
|
|
29
|
-
type TPrimitiveValue = string | boolean | number | undefined | TPrimitiveValue[] | {
|
|
30
|
-
[key: string]: TPrimitiveValue
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type TExpirationDelay = 'never' | string | number | Date;
|
|
34
|
-
|
|
35
|
-
type CacheEntry<TValue extends TPrimitiveValue =TPrimitiveValue > = {
|
|
36
|
-
// Value
|
|
37
|
-
value: TValue,
|
|
38
|
-
// Expiration Timestamp
|
|
39
|
-
expiration?: number,
|
|
40
|
-
changes: number
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type TCacheGetOrUpdateArgs<TValeur extends TPrimitiveValue> = [
|
|
44
|
-
cle: string,
|
|
45
|
-
func: (() => Promise<TValeur>),
|
|
46
|
-
expiration?: TExpirationDelay,
|
|
47
|
-
avecDetails?: boolean
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
type TCacheGetOnlyArgs = [
|
|
51
|
-
cle: string,
|
|
52
|
-
avecDetails: true
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
/*----------------------------------
|
|
56
|
-
- TYPES
|
|
57
|
-
----------------------------------*/
|
|
58
|
-
|
|
59
|
-
export type Config = {
|
|
60
|
-
debug: boolean,
|
|
61
|
-
disk: string, // TODO: keyof disks
|
|
62
|
-
disks: DisksManager
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type Hooks = {
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/*----------------------------------
|
|
70
|
-
- SERVICE
|
|
71
|
-
----------------------------------*/
|
|
72
|
-
export default class Cache extends Service<Config, Hooks, Application> {
|
|
73
|
-
|
|
74
|
-
public commands = registerCommands(this);
|
|
75
|
-
|
|
76
|
-
public data: {[key: string]: CacheEntry | undefined} = {};
|
|
77
|
-
|
|
78
|
-
private disk: Driver;
|
|
79
|
-
|
|
80
|
-
public constructor(
|
|
81
|
-
parent: AnyService,
|
|
82
|
-
config: Config,
|
|
83
|
-
app: Application,
|
|
84
|
-
) {
|
|
85
|
-
|
|
86
|
-
super(parent, config, app);
|
|
87
|
-
|
|
88
|
-
this.disk = this.config.disks.get(config.disk)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/*----------------------------------
|
|
92
|
-
- LIFECYCLE
|
|
93
|
-
----------------------------------*/
|
|
94
|
-
|
|
95
|
-
public async ready() {
|
|
96
|
-
|
|
97
|
-
setInterval(() => this.cleanMem(), 10000);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
public async shutdown() {
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/*----------------------------------
|
|
105
|
-
- ACTIONS
|
|
106
|
-
----------------------------------*/
|
|
107
|
-
private async restore() {
|
|
108
|
-
const files = await this.disk.readDir('data', 'cache')
|
|
109
|
-
for (const file of files) {
|
|
110
|
-
|
|
111
|
-
if (!file.name.endsWith('.json'))
|
|
112
|
-
continue;
|
|
113
|
-
|
|
114
|
-
const entryKey = file.name.substring(0, file.name.length - 5);
|
|
115
|
-
const filePath = path.join('cache', file.name);
|
|
116
|
-
this.data[ entryKey ] = await this.disk.readJSON('data', filePath);
|
|
117
|
-
console.log(LogPrefix, `Restored cache entry ${entryKey}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private cleanMem() {
|
|
122
|
-
|
|
123
|
-
// Remove expired data
|
|
124
|
-
const now = Date.now();
|
|
125
|
-
for (const key in this.data) {
|
|
126
|
-
const entry = this.data[ key ];
|
|
127
|
-
if (entry?.expiration && entry.expiration < now) {
|
|
128
|
-
this.config.debug && console.log(LogPrefix, `Delete expired data: ${key}`);
|
|
129
|
-
this.del(key);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Write changes
|
|
134
|
-
for (const entryKey in this.data) {
|
|
135
|
-
|
|
136
|
-
const entry = this.data[entryKey];
|
|
137
|
-
if (!entry?.changes)
|
|
138
|
-
continue;
|
|
139
|
-
|
|
140
|
-
this.config.debug && console.log(LogPrefix, `Flush ${entry.changes} changes for ${entryKey}`);
|
|
141
|
-
|
|
142
|
-
entry.changes = 0;
|
|
143
|
-
const entryFile = this.getEntryFile(entryKey);
|
|
144
|
-
fs.outputJSONSync(entryFile , this.data[entryKey]);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private getEntryFile( entryKey: string ) {
|
|
149
|
-
return path.join(this.cacheDir, entryKey + '.json');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
public get<TValeur extends TPrimitiveValue>(
|
|
153
|
-
cle: string,
|
|
154
|
-
avecDetails?: true
|
|
155
|
-
): Promise<CacheEntry<TValeur> | TValeur | undefined>;
|
|
156
|
-
|
|
157
|
-
// Expiration = Durée de vie en secondes ou date max
|
|
158
|
-
// Retourne null quand pas de valeur
|
|
159
|
-
public get<TValeur extends TPrimitiveValue>(
|
|
160
|
-
cle: string,
|
|
161
|
-
func: (() => Promise<TValeur>),
|
|
162
|
-
expiration: TExpirationDelay,
|
|
163
|
-
avecDetails: true
|
|
164
|
-
): Promise<CacheEntry<TValeur>>;
|
|
165
|
-
|
|
166
|
-
public get<TValeur extends TPrimitiveValue>(
|
|
167
|
-
cle: string,
|
|
168
|
-
func: (() => Promise<TValeur>),
|
|
169
|
-
expiration?: TExpirationDelay,
|
|
170
|
-
avecDetails?: false
|
|
171
|
-
): Promise<TValeur>;
|
|
172
|
-
|
|
173
|
-
public async get<TValeur extends TPrimitiveValue, TArgs extends TCacheGetOnlyArgs | TCacheGetOrUpdateArgs<TValeur> = TCacheGetOnlyArgs | TCacheGetOrUpdateArgs<TValeur>>(
|
|
174
|
-
...args: TArgs
|
|
175
|
-
): Promise< TValeur | CacheEntry<TValeur> | (TArgs extends TCacheGetOnlyArgs ? undefined : TValeur)> {
|
|
176
|
-
|
|
177
|
-
let cle: string;
|
|
178
|
-
let func: (() => Promise<TValeur>) | undefined;
|
|
179
|
-
let expiration: TExpirationDelay | undefined;
|
|
180
|
-
let avecDetails: boolean | undefined = true;
|
|
181
|
-
|
|
182
|
-
if (typeof args[1] === 'function') {
|
|
183
|
-
([ cle, func, expiration, avecDetails ] = args);
|
|
184
|
-
} else {
|
|
185
|
-
([ cle, avecDetails ] = args);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (expiration === undefined)
|
|
189
|
-
expiration = 'never';
|
|
190
|
-
|
|
191
|
-
let entry: CacheEntry<TValeur> | undefined = this.data[cle];
|
|
192
|
-
|
|
193
|
-
// Expired
|
|
194
|
-
if (entry?.expiration && entry.expiration < Date.now()){
|
|
195
|
-
this.config.debug && console.log(LogPrefix, `Key ${cle} expired.`);
|
|
196
|
-
entry = undefined;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Donnée inexistante
|
|
200
|
-
if (entry !== undefined) {
|
|
201
|
-
|
|
202
|
-
this.config.debug && console.log(LogPrefix, `Get "${cle}": restored via cache`);
|
|
203
|
-
|
|
204
|
-
} else if (func !== undefined) {
|
|
205
|
-
|
|
206
|
-
this.config.debug && console.log(LogPrefix, `Get "${cle}": refresh value`);
|
|
207
|
-
|
|
208
|
-
// Rechargement
|
|
209
|
-
entry = {
|
|
210
|
-
value: await func(),
|
|
211
|
-
expiration: this.delayToTimestamp(expiration),
|
|
212
|
-
changes: 0
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (expiration !== 'now')
|
|
216
|
-
await this.set(cle, entry.value, expiration);
|
|
217
|
-
|
|
218
|
-
} else
|
|
219
|
-
return undefined;
|
|
220
|
-
|
|
221
|
-
return avecDetails
|
|
222
|
-
? entry
|
|
223
|
-
: entry.value as TValeur;
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Put in cache a JSON value, associated with an unique ID.
|
|
228
|
-
* @param cle Unique identifier for the cache entry. Used to retrieve the value via Cache.set()
|
|
229
|
-
* @param val The value to put in cache
|
|
230
|
-
* @param expiration The interval in which the data is valid.
|
|
231
|
-
* - string: the humain-readable expression. Exemple: 10 minutes
|
|
232
|
-
* - number: time in seconds
|
|
233
|
-
* - Date: the date at which the data expires
|
|
234
|
-
* - null: no expiration (default)
|
|
235
|
-
* @returns A void promise
|
|
236
|
-
*/
|
|
237
|
-
public set( cle: string, val: TPrimitiveValue, expiration: TExpirationDelay = 'never' ): void {
|
|
238
|
-
|
|
239
|
-
// TODO: check is key contains illegal characters
|
|
240
|
-
|
|
241
|
-
this.config.debug && console.log(LogPrefix, "Updating cache " + cle);
|
|
242
|
-
this.data[ cle ] = {
|
|
243
|
-
value: val,
|
|
244
|
-
expiration: this.delayToTimestamp(expiration),
|
|
245
|
-
changes: 1
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
public del( key: string ): void {
|
|
250
|
-
|
|
251
|
-
if (key === undefined) {
|
|
252
|
-
this.data = {};
|
|
253
|
-
console.log(LogPrefix, "Deleting all keys from cache");
|
|
254
|
-
fs.removeSync( this.cacheDir );
|
|
255
|
-
} else {
|
|
256
|
-
this.data[ key ] = undefined;
|
|
257
|
-
console.log(LogPrefix, `Deleting key "${key}" from cache`);
|
|
258
|
-
const entryFile = this.getEntryFile(key);
|
|
259
|
-
fs.removeSync( entryFile );
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
/*----------------------------------
|
|
265
|
-
- UTILS
|
|
266
|
-
----------------------------------*/
|
|
267
|
-
/**
|
|
268
|
-
*
|
|
269
|
-
* @param delay
|
|
270
|
-
* @returns number (timestamp when the data expired) or undefined (never expires)
|
|
271
|
-
*/
|
|
272
|
-
private delayToTimestamp( delay: TExpirationDelay ): number | undefined {
|
|
273
|
-
|
|
274
|
-
if (delay === 'now') {
|
|
275
|
-
|
|
276
|
-
return Date.now();
|
|
277
|
-
|
|
278
|
-
} else if (delay === 'never') {
|
|
279
|
-
|
|
280
|
-
return undefined;
|
|
281
|
-
|
|
282
|
-
// H expression
|
|
283
|
-
} else if (typeof delay === 'string') {
|
|
284
|
-
|
|
285
|
-
const ms = hInterval(delay);
|
|
286
|
-
if (ms === undefined) throw new Error(`Invalid period string: ` + delay);
|
|
287
|
-
return Date.now() + ms;
|
|
288
|
-
|
|
289
|
-
// Lifetime in seconds
|
|
290
|
-
} else if (typeof delay === 'number')
|
|
291
|
-
return Date.now() + delay;
|
|
292
|
-
|
|
293
|
-
// Date limit
|
|
294
|
-
else
|
|
295
|
-
return delay.getTime();
|
|
296
|
-
}
|
|
297
|
-
}
|