@mxfb/tools 1.0.0 → 1.0.3
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/TODO.md +1 -0
- package/agnostic/@publisher/index.d.ts +47 -0
- package/agnostic/@publisher/index.js +361 -0
- package/agnostic/elo/index.d.ts +23 -0
- package/agnostic/elo/index.js +28 -0
- package/agnostic/elo/index.test.js +67 -0
- package/agnostic/index.d.ts +5 -0
- package/agnostic/index.js +5 -0
- package/agnostic/poetry/generate-rhymes/index.d.ts +1 -0
- package/agnostic/poetry/generate-rhymes/index.js +125 -0
- package/agnostic/poetry/index.d.ts +1 -0
- package/agnostic/poetry/index.js +1 -0
- package/agnostic/scrapers/@meteociel/index.js +240 -186
- package/agnostic/scrapers/@meteociel/index.test.js +43 -0
- package/agnostic/scrapers/@meteociel/locations/index.js +9 -5
- package/agnostic/scrapers/@meteociel/types.js +1 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.d.ts +22 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.js +124 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.d.ts +1 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.js +107 -0
- package/agnostic/scrapers/@tumblr/blogs/index.d.ts +8 -0
- package/agnostic/scrapers/@tumblr/blogs/index.js +23 -0
- package/agnostic/scrapers/@tumblr/index.d.ts +73 -0
- package/agnostic/scrapers/@tumblr/index.js +180 -0
- package/agnostic/scrapers/@tumblr/index.test.d.ts +1 -0
- package/agnostic/scrapers/@tumblr/index.test.js +319 -0
- package/agnostic/scrapers/@tumblr/types.d.ts +238 -0
- package/agnostic/scrapers/@tumblr/types.js +38 -0
- package/agnostic/scrapers/@tumblr/utils.d.ts +12 -0
- package/agnostic/scrapers/@tumblr/utils.js +407 -0
- package/agnostic/scrapers/index.d.ts +2 -0
- package/agnostic/scrapers/index.js +2 -0
- package/agnostic/weather/index.d.ts +1 -0
- package/agnostic/weather/index.js +1 -0
- package/agnostic/weather/types.d.ts +2 -2
- package/agnostic/weather/types.js +8 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +1 -0
- package/components/public-classnames.d.ts +5 -0
- package/components/public-classnames.js +5 -0
- package/components/utils/index.d.ts +1 -0
- package/components/utils/index.js +12 -0
- package/components/utils/types.d.ts +3 -0
- package/components/utils/types.js +1 -0
- package/index.d.ts +3 -0
- package/index.js +3 -0
- package/node/headless-browser/index.d.ts +37 -0
- package/node/headless-browser/index.js +126 -0
- package/node/headless-browser/types.d.ts +47 -0
- package/node/headless-browser/types.js +5 -0
- package/node/index.js +1 -0
- package/node/index.test.d.ts +1 -0
- package/node/index.test.js +6 -0
- package/node/scrapers/@tumblr/index.d.ts +61 -0
- package/node/scrapers/@tumblr/index.js +182 -0
- package/node/scrapers/@tumblr/index.test.d.ts +1 -0
- package/node/scrapers/@tumblr/index.test.js +185 -0
- package/node/scrapers/@tumblr/types.d.ts +91 -0
- package/node/scrapers/@tumblr/types.js +1 -0
- package/node/scrapers/@tumblr/utils.d.ts +23 -0
- package/node/scrapers/@tumblr/utils.js +328 -0
- package/node/scrapers/index.d.ts +1 -0
- package/node/scrapers/index.js +1 -0
- package/package.json +197 -31
- package/chunks/chunk-JQLUR5LO.js +0 -12
- package/components/Comp/index.js +0 -0
- /package/{components/Comp/index.d.ts → agnostic/elo/index.test.d.ts} +0 -0
package/TODO.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# To do
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Core, type EndpointName, type ContractOf, type BodyOf, type ParamsOf, type QueryOf, type FileFieldsOf, type ResponseOf, type ErrorResponseOf } from '@design-edito/publisher';
|
|
2
|
+
type Options<E extends EndpointName> = (BodyOf<E> extends null ? {} : {
|
|
3
|
+
body: BodyOf<E>;
|
|
4
|
+
}) & (QueryOf<E> extends null ? {} : {
|
|
5
|
+
query: QueryOf<E>;
|
|
6
|
+
}) & (ParamsOf<E> extends null ? {} : {
|
|
7
|
+
params: ParamsOf<E>;
|
|
8
|
+
}) & (FileFieldsOf<E> extends null ? {} : {
|
|
9
|
+
files: Partial<Record<NonNullable<FileFieldsOf<E>>, Array<File>>>;
|
|
10
|
+
});
|
|
11
|
+
type IsOptionsOptional<E extends EndpointName> = BodyOf<E> extends null ? (QueryOf<E> extends null ? (ParamsOf<E> extends null ? (FileFieldsOf<E> extends null ? true : false) : false) : false) : false;
|
|
12
|
+
type FormattedResponse<E extends EndpointName> = undefined | Blob | ResponseOf<E>;
|
|
13
|
+
type FetchParams<E extends EndpointName> = IsOptionsOptional<E> extends true ? [endpoint: E, options?: Options<E>, previousErrors?: Core.Errors.Codes[]] : [endpoint: E, options: Options<E>, previousErrors?: Core.Errors.Codes[]];
|
|
14
|
+
type OnRequestCallback = (request: {
|
|
15
|
+
url: string;
|
|
16
|
+
endpoint: EndpointName;
|
|
17
|
+
requestInit: RequestInit;
|
|
18
|
+
}) => void;
|
|
19
|
+
type OnResponseCallback = (response: FormattedResponse<EndpointName>, request: {
|
|
20
|
+
url: string;
|
|
21
|
+
endpoint: EndpointName;
|
|
22
|
+
requestInit: RequestInit;
|
|
23
|
+
}) => void;
|
|
24
|
+
type RequesterOptions = {
|
|
25
|
+
onRequest?: OnRequestCallback;
|
|
26
|
+
onResponse?: OnResponseCallback;
|
|
27
|
+
accessTokenLocalStorageKey?: string;
|
|
28
|
+
};
|
|
29
|
+
export default class Requester {
|
|
30
|
+
private baseUrl;
|
|
31
|
+
private onRequest;
|
|
32
|
+
private onResponse;
|
|
33
|
+
private storedAccessTokenKey;
|
|
34
|
+
private csrfToken;
|
|
35
|
+
private accessToken;
|
|
36
|
+
constructor(baseUrl: string, options?: RequesterOptions);
|
|
37
|
+
private setCsrfToken;
|
|
38
|
+
private setAccessToken;
|
|
39
|
+
private getServerResponse;
|
|
40
|
+
private csrfRefreshPromise;
|
|
41
|
+
private formatServerResponse;
|
|
42
|
+
private refreshCsrfToken;
|
|
43
|
+
private refreshAccessToken;
|
|
44
|
+
fetch<E extends EndpointName>(...args: FetchParams<E>): Promise<ContractOf<E>['type'] extends 'void' ? undefined | ErrorResponseOf<E> : ContractOf<E>['type'] extends 'file' ? Blob | ErrorResponseOf<E> : ResponseOf<E>>;
|
|
45
|
+
private contractsDetails;
|
|
46
|
+
}
|
|
47
|
+
export { Requester };
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/* Linting is suspended on this file: 44 of its 74 findings come from the deferred
|
|
2
|
+
conditional response type and cannot be fixed without reworking it. See « Two files
|
|
3
|
+
opted out of linting » in README.md before removing this. */
|
|
4
|
+
/* eslint-disable
|
|
5
|
+
@typescript-eslint/array-type,
|
|
6
|
+
@typescript-eslint/class-methods-use-this,
|
|
7
|
+
@typescript-eslint/consistent-type-assertions,
|
|
8
|
+
@typescript-eslint/dot-notation,
|
|
9
|
+
@typescript-eslint/explicit-function-return-type,
|
|
10
|
+
@typescript-eslint/no-empty-object-type,
|
|
11
|
+
@typescript-eslint/no-explicit-any,
|
|
12
|
+
@typescript-eslint/no-unnecessary-condition,
|
|
13
|
+
@typescript-eslint/no-unnecessary-type-assertion,
|
|
14
|
+
@typescript-eslint/no-unsafe-argument,
|
|
15
|
+
@typescript-eslint/no-unsafe-assignment,
|
|
16
|
+
@typescript-eslint/no-unsafe-member-access,
|
|
17
|
+
@typescript-eslint/no-unsafe-return,
|
|
18
|
+
@typescript-eslint/no-unsafe-type-assertion,
|
|
19
|
+
@typescript-eslint/prefer-nullish-coalescing,
|
|
20
|
+
@typescript-eslint/prefer-readonly,
|
|
21
|
+
@typescript-eslint/return-await,
|
|
22
|
+
@typescript-eslint/strict-boolean-expressions,
|
|
23
|
+
object-shorthand,
|
|
24
|
+
preserve-caught-error,
|
|
25
|
+
require-unicode-regexp
|
|
26
|
+
*/
|
|
27
|
+
import { minutes } from '@design-edito/tools/agnostic/time/duration/index.js';
|
|
28
|
+
import { unknownToString } from '@design-edito/tools/agnostic/errors/unknown-to-string/index.js';
|
|
29
|
+
import { Core, Endpoint } from '@design-edito/publisher';
|
|
30
|
+
const { Codes } = Core.Errors;
|
|
31
|
+
export default class Requester {
|
|
32
|
+
baseUrl;
|
|
33
|
+
onRequest;
|
|
34
|
+
onResponse;
|
|
35
|
+
storedAccessTokenKey;
|
|
36
|
+
csrfToken = { value: null, fetchedAt: null };
|
|
37
|
+
accessToken = { value: null, fetchedAt: null };
|
|
38
|
+
constructor(baseUrl, options) {
|
|
39
|
+
this.baseUrl = baseUrl;
|
|
40
|
+
this.onRequest = options?.onRequest;
|
|
41
|
+
this.onResponse = options?.onResponse;
|
|
42
|
+
this.fetch = this.fetch.bind(this);
|
|
43
|
+
this.storedAccessTokenKey = options?.accessTokenLocalStorageKey ?? 'access_token';
|
|
44
|
+
const storedToken = localStorage.getItem(`${this.storedAccessTokenKey}/value`);
|
|
45
|
+
const storedFetchedAt = localStorage.getItem(`${this.storedAccessTokenKey}/fetchedAt`);
|
|
46
|
+
if (storedToken) {
|
|
47
|
+
this.accessToken.value = storedToken;
|
|
48
|
+
this.accessToken.fetchedAt = storedFetchedAt ? parseInt(storedFetchedAt) : null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
setCsrfToken(token) {
|
|
52
|
+
// console.group('setCsrfToken')
|
|
53
|
+
// console.log('->', token)
|
|
54
|
+
this.csrfToken.value = token;
|
|
55
|
+
this.csrfToken.fetchedAt = token !== null ? Date.now() : null;
|
|
56
|
+
// console.log('<-', this.csrfToken)
|
|
57
|
+
// console.groupEnd()
|
|
58
|
+
}
|
|
59
|
+
setAccessToken(token) {
|
|
60
|
+
// console.group('setAccessToken')
|
|
61
|
+
// console.log('->', token)
|
|
62
|
+
this.accessToken.value = token;
|
|
63
|
+
const fetchedAt = Date.now();
|
|
64
|
+
this.accessToken.fetchedAt = token !== null ? fetchedAt : null;
|
|
65
|
+
if (token !== null) {
|
|
66
|
+
localStorage.setItem(`${this.storedAccessTokenKey}/value`, token);
|
|
67
|
+
localStorage.setItem(`${this.storedAccessTokenKey}/fetchedAt`, `${fetchedAt}`);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
localStorage.removeItem(`${this.storedAccessTokenKey}/value`);
|
|
71
|
+
localStorage.removeItem(`${this.storedAccessTokenKey}/fetchedAt`);
|
|
72
|
+
}
|
|
73
|
+
// console.log('<-', this.accessToken)
|
|
74
|
+
// console.groupEnd()
|
|
75
|
+
}
|
|
76
|
+
async getServerResponse(...args) {
|
|
77
|
+
// console.group('getServerResponse')
|
|
78
|
+
const [endpoint, options = {}] = args;
|
|
79
|
+
// console.log('->', { endpoint, options })
|
|
80
|
+
// Route
|
|
81
|
+
const route = Endpoint[endpoint];
|
|
82
|
+
// Method
|
|
83
|
+
const [rawMethod, ...rawPathColonSplitted] = route.split(':');
|
|
84
|
+
const method = rawMethod?.toUpperCase() ?? '';
|
|
85
|
+
if (method !== 'GET'
|
|
86
|
+
&& method !== 'POST'
|
|
87
|
+
&& method !== 'PUT'
|
|
88
|
+
&& method !== 'DELETE')
|
|
89
|
+
throw new Error(`Invalid method: ${method}`);
|
|
90
|
+
// Path
|
|
91
|
+
const pathChunks = rawPathColonSplitted
|
|
92
|
+
.join(':')
|
|
93
|
+
.replace(/^\/+/, '')
|
|
94
|
+
.split('/');
|
|
95
|
+
const path = pathChunks.join('/');
|
|
96
|
+
// Payload
|
|
97
|
+
const { body: rawBody, query: rawQuery, params: rawParams, files: rawFiles } = options;
|
|
98
|
+
const { type, upload } = this.contractsDetails[endpoint];
|
|
99
|
+
const body = rawBody ?? undefined;
|
|
100
|
+
const query = rawQuery ?? undefined;
|
|
101
|
+
const params = (rawParams ?? undefined);
|
|
102
|
+
const files = rawFiles ?? {};
|
|
103
|
+
// Params
|
|
104
|
+
let pathString = path;
|
|
105
|
+
pathString = pathString.replace(/:([A-Za-z0-9_]+)/g, (_, key) => {
|
|
106
|
+
return params?.[key] !== undefined
|
|
107
|
+
? params[key]
|
|
108
|
+
: `:${key}`;
|
|
109
|
+
});
|
|
110
|
+
// Query
|
|
111
|
+
let queryString = '';
|
|
112
|
+
Object
|
|
113
|
+
.entries(query ?? {})
|
|
114
|
+
.forEach(([key, value]) => {
|
|
115
|
+
const val = Array.isArray(value) ? value : [value];
|
|
116
|
+
val.forEach(v => {
|
|
117
|
+
queryString += `${key}=${v}&`;
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
queryString = queryString.slice(0, -1);
|
|
121
|
+
// URL
|
|
122
|
+
const url = new URL(pathString, this.baseUrl);
|
|
123
|
+
url.search = queryString;
|
|
124
|
+
// Form data
|
|
125
|
+
const formData = new FormData();
|
|
126
|
+
Object
|
|
127
|
+
.entries(files)
|
|
128
|
+
.forEach(([key, value]) => value?.forEach(file => formData.append(key, file)));
|
|
129
|
+
formData.append('_json', JSON.stringify(body ?? {}));
|
|
130
|
+
// Fetch
|
|
131
|
+
const headers = {};
|
|
132
|
+
if (this.accessToken.value !== null) {
|
|
133
|
+
headers['Authorization'] = `Bearer ${this.accessToken.value}`;
|
|
134
|
+
}
|
|
135
|
+
if (this.csrfToken.value !== null) {
|
|
136
|
+
headers['x-csrf-token'] = this.csrfToken.value;
|
|
137
|
+
}
|
|
138
|
+
if (!upload) {
|
|
139
|
+
headers['Content-Type'] = 'application/json';
|
|
140
|
+
}
|
|
141
|
+
if (type === 'file') {
|
|
142
|
+
headers['Accept'] = 'application/octet-stream';
|
|
143
|
+
}
|
|
144
|
+
else if (type === 'void') {
|
|
145
|
+
delete headers['Accept'];
|
|
146
|
+
}
|
|
147
|
+
else if (type === 'json') {
|
|
148
|
+
headers['Accept'] = 'application/json';
|
|
149
|
+
}
|
|
150
|
+
else if (type === 'list') {
|
|
151
|
+
headers['Accept'] = 'application/json';
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
throw new Error(`Invalid type: ${type}`);
|
|
155
|
+
}
|
|
156
|
+
const reqUrl = url.toString();
|
|
157
|
+
const requestInit = {
|
|
158
|
+
method,
|
|
159
|
+
credentials: 'include',
|
|
160
|
+
body: method !== 'GET'
|
|
161
|
+
? (upload ? formData : JSON.stringify(body ?? {}))
|
|
162
|
+
: undefined,
|
|
163
|
+
headers: headers
|
|
164
|
+
};
|
|
165
|
+
// console.log({ url, endpoint, headers })
|
|
166
|
+
const requestData = { url: reqUrl, endpoint, requestInit };
|
|
167
|
+
this.onRequest?.(requestData);
|
|
168
|
+
const res = await window.fetch(reqUrl, requestInit);
|
|
169
|
+
// Check returned access tokens
|
|
170
|
+
const rawAccessToken = res.headers.get('x-access-token');
|
|
171
|
+
// console.log({ respondedToken: rawAccessToken })
|
|
172
|
+
if (rawAccessToken !== null) {
|
|
173
|
+
const accessToken = rawAccessToken.split(/\s+/g).at(1) ?? null;
|
|
174
|
+
if (accessToken !== this.accessToken.value)
|
|
175
|
+
this.setAccessToken(accessToken);
|
|
176
|
+
}
|
|
177
|
+
const formattedResponse = await this.formatServerResponse(res, type);
|
|
178
|
+
this.onResponse?.(formattedResponse, requestData);
|
|
179
|
+
// console.log('<-', res)
|
|
180
|
+
// console.groupEnd()
|
|
181
|
+
return res;
|
|
182
|
+
}
|
|
183
|
+
csrfRefreshPromise = null;
|
|
184
|
+
async formatServerResponse(res, type) {
|
|
185
|
+
if (type === 'void')
|
|
186
|
+
return res.status === 204
|
|
187
|
+
? undefined
|
|
188
|
+
: await res.clone().json();
|
|
189
|
+
else if (type === 'file') {
|
|
190
|
+
const contentType = res.headers.get('Content-Type');
|
|
191
|
+
if (contentType === 'application/json')
|
|
192
|
+
return await res.clone().json();
|
|
193
|
+
return await res.blob();
|
|
194
|
+
}
|
|
195
|
+
else if (type === 'json')
|
|
196
|
+
return await res.clone().json();
|
|
197
|
+
else if (type === 'list')
|
|
198
|
+
return await res.clone().json();
|
|
199
|
+
else
|
|
200
|
+
throw new Error(`Invalid endpoint type: ${type}`);
|
|
201
|
+
}
|
|
202
|
+
async refreshCsrfToken() {
|
|
203
|
+
// console.group('refreshCsrfToken')
|
|
204
|
+
if (this.csrfRefreshPromise !== null) {
|
|
205
|
+
// console.log('already pending')
|
|
206
|
+
// console.groupEnd()
|
|
207
|
+
return this.csrfRefreshPromise;
|
|
208
|
+
}
|
|
209
|
+
this.csrfRefreshPromise = (async () => {
|
|
210
|
+
const res = await this.getServerResponse('CSRF_GET_TOKEN');
|
|
211
|
+
const formattedRes = await this.formatServerResponse(res, 'json');
|
|
212
|
+
if (!res.ok || !formattedRes.success) {
|
|
213
|
+
this.setCsrfToken(null);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
const newToken = formattedRes.payload.token;
|
|
217
|
+
this.setCsrfToken(newToken);
|
|
218
|
+
return true;
|
|
219
|
+
})().finally(() => {
|
|
220
|
+
this.csrfRefreshPromise = null;
|
|
221
|
+
});
|
|
222
|
+
// console.log(this.csrfRefreshPromise)
|
|
223
|
+
// console.groupEnd()
|
|
224
|
+
return this.csrfRefreshPromise;
|
|
225
|
+
}
|
|
226
|
+
async refreshAccessToken() {
|
|
227
|
+
// console.group('refreshAccessToken')
|
|
228
|
+
const res = await this.getServerResponse('AUTH_REFRESH_TOKEN');
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
this.setAccessToken(null);
|
|
231
|
+
// console.groupEnd()
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const rawToken = res.headers.get('x-access-token');
|
|
235
|
+
if (rawToken === null) {
|
|
236
|
+
this.setAccessToken(null);
|
|
237
|
+
// console.groupEnd()
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const token = rawToken.split(/\s+/g).at(1) ?? '';
|
|
241
|
+
if (token !== this.accessToken.value)
|
|
242
|
+
this.setAccessToken(token);
|
|
243
|
+
// console.groupEnd()
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
async fetch(...args) {
|
|
247
|
+
// console.group('fetch')
|
|
248
|
+
const [endpoint, optionsOrPrevErrors = {}, _previousErrors = []] = args;
|
|
249
|
+
const options = Array.isArray(optionsOrPrevErrors)
|
|
250
|
+
? {}
|
|
251
|
+
: optionsOrPrevErrors;
|
|
252
|
+
const previousErrors = Array.isArray(optionsOrPrevErrors)
|
|
253
|
+
? optionsOrPrevErrors
|
|
254
|
+
: _previousErrors;
|
|
255
|
+
// console.log('->', { endpoint, options, previousErrors })
|
|
256
|
+
const { type, auth } = this.contractsDetails[endpoint];
|
|
257
|
+
// Refresh CSRF token if needed
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
if (this.csrfToken.value === null
|
|
260
|
+
|| this.csrfToken.fetchedAt === null
|
|
261
|
+
|| now - this.csrfToken.fetchedAt > minutes(20).toMilliseconds())
|
|
262
|
+
await this.refreshCsrfToken();
|
|
263
|
+
// Refresh access token if needed
|
|
264
|
+
if (auth !== 'none' && this.accessToken.value === null)
|
|
265
|
+
await this.refreshAccessToken();
|
|
266
|
+
let res;
|
|
267
|
+
let formatted;
|
|
268
|
+
try {
|
|
269
|
+
res = await this.getServerResponse(endpoint, options);
|
|
270
|
+
formatted = await this.formatServerResponse(res, type);
|
|
271
|
+
if (endpoint === 'AUTH_LOGOUT'
|
|
272
|
+
|| endpoint === 'AUTH_LOGOUT_EVERYWHERE')
|
|
273
|
+
this.setAccessToken(null);
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
throw new Error(`Request failed: ${unknownToString(err)}`);
|
|
277
|
+
}
|
|
278
|
+
if (formatted === undefined) { /*console.log('<-', formatted); console.groupEnd(); */
|
|
279
|
+
return formatted;
|
|
280
|
+
}
|
|
281
|
+
if (formatted instanceof Blob) { /*console.log('<-', formatted); console.groupEnd(); */
|
|
282
|
+
return formatted;
|
|
283
|
+
}
|
|
284
|
+
if (formatted.success) { /*console.log('<-', formatted); console.groupEnd();*/
|
|
285
|
+
return formatted;
|
|
286
|
+
}
|
|
287
|
+
if (previousErrors.includes(formatted.error.code)) { /*console.log('<-', formatted); console.groupEnd();*/
|
|
288
|
+
return formatted;
|
|
289
|
+
}
|
|
290
|
+
const newPreviousErrors = [...previousErrors, formatted.error.code];
|
|
291
|
+
// Invalid CSRF token
|
|
292
|
+
if (formatted.error.code === Codes.INVALID_CSRF_TOKEN) {
|
|
293
|
+
await this.refreshCsrfToken();
|
|
294
|
+
// console.log('<-', formatted)
|
|
295
|
+
// console.groupEnd()
|
|
296
|
+
return this.fetch(endpoint, options, newPreviousErrors);
|
|
297
|
+
}
|
|
298
|
+
// Invalid access token
|
|
299
|
+
if (formatted.error.code === Codes.INVALID_ACCESS_TOKEN) {
|
|
300
|
+
await this.refreshAccessToken();
|
|
301
|
+
// console.log('<-', formatted)
|
|
302
|
+
// console.groupEnd()
|
|
303
|
+
return this.fetch(endpoint, options, newPreviousErrors);
|
|
304
|
+
}
|
|
305
|
+
// console.log('<-', formatted)
|
|
306
|
+
// console.groupEnd()
|
|
307
|
+
return formatted;
|
|
308
|
+
}
|
|
309
|
+
contractsDetails = {
|
|
310
|
+
// Core
|
|
311
|
+
ADMIN_TEMP_FLUSH: { type: 'void', auth: 'strong', upload: false },
|
|
312
|
+
ADMIN_USERS_CREATE: { type: 'json', auth: 'strong', upload: false },
|
|
313
|
+
ADMIN_USERS_DELETE: { type: 'json', auth: 'strong', upload: false },
|
|
314
|
+
ADMIN_USERS_GET: { type: 'json', auth: 'strong', upload: false },
|
|
315
|
+
ADMIN_USERS_GET_UPLOAD_QUOTA: { type: 'json', auth: 'strong', upload: false },
|
|
316
|
+
ADMIN_USERS_LIST: { type: 'list', auth: 'strong', upload: false },
|
|
317
|
+
ADMIN_USERS_RESET_UPLOAD_QUOTA: { type: 'json', auth: 'strong', upload: false },
|
|
318
|
+
ADMIN_USERS_REVOKE_AUTH_TOKENS: { type: 'void', auth: 'strong', upload: false },
|
|
319
|
+
ADMIN_USERS_REVOKE_EMAIL_VALIDATION_TOKENS: { type: 'void', auth: 'strong', upload: false },
|
|
320
|
+
ADMIN_USERS_REVOKE_PASSWORD_RENEWAL_TOKENS: { type: 'void', auth: 'strong', upload: false },
|
|
321
|
+
ADMIN_USERS_UPDATE: { type: 'json', auth: 'strong', upload: false },
|
|
322
|
+
AUTH_LOGIN: { type: 'json', auth: 'none', upload: false },
|
|
323
|
+
AUTH_LOGOUT: { type: 'void', auth: 'weak', upload: false },
|
|
324
|
+
AUTH_LOGOUT_EVERYWHERE: { type: 'void', auth: 'weak', upload: false },
|
|
325
|
+
AUTH_REFRESH_TOKEN: { type: 'void', auth: 'none', upload: false },
|
|
326
|
+
AUTH_REQUEST_EMAIL_VERIFICATION_TOKEN: { type: 'void', auth: 'none', upload: false },
|
|
327
|
+
AUTH_REQUEST_NEW_PASSWORD: { type: 'void', auth: 'none', upload: false },
|
|
328
|
+
AUTH_SIGNUP: { type: 'json', auth: 'none', upload: false },
|
|
329
|
+
AUTH_SUBMIT_NEW_PASSWORD: { type: 'void', auth: 'none', upload: false },
|
|
330
|
+
AUTH_VERIFY_EMAIL: { type: 'json', auth: 'none', upload: false },
|
|
331
|
+
AUTH_WHOAMI: { type: 'json', auth: 'strong', upload: false },
|
|
332
|
+
AUTH_WHOAMI_GET: { type: 'json', auth: 'strong', upload: false },
|
|
333
|
+
CSRF_GET_TOKEN: { type: 'json', auth: 'none', upload: false },
|
|
334
|
+
EXAMPLE_PLUGIN__SOMETHING: { type: 'json', auth: 'none', upload: false },
|
|
335
|
+
IMAGE_FORMAT: { type: 'file', auth: 'strong', upload: true },
|
|
336
|
+
IMAGE_TRANSFORM: { type: 'file', auth: 'strong', upload: true },
|
|
337
|
+
SYSTEM_KILL: { type: 'void', auth: 'strong', upload: false },
|
|
338
|
+
SYSTEM_PING: { type: 'json', auth: 'none', upload: false },
|
|
339
|
+
SYSTEM_SEND_MAIL: { type: 'void', auth: 'strong', upload: false },
|
|
340
|
+
SYSTEM_STATUS_CHECK_GET: { type: 'json', auth: 'none', upload: false },
|
|
341
|
+
SYSTEM_STATUS_CHECK_POST: { type: 'json', auth: 'none', upload: false },
|
|
342
|
+
USERS_GET: { type: 'json', auth: 'none', upload: false },
|
|
343
|
+
// Plugins
|
|
344
|
+
TEST_PLUGIN__FILE: { type: 'file', auth: 'none', upload: false },
|
|
345
|
+
TEST_PLUGIN__FILE_POST: { type: 'file', auth: 'none', upload: false },
|
|
346
|
+
TEST_PLUGIN__FILE_POST_AUTH: { type: 'file', auth: 'strong', upload: false },
|
|
347
|
+
TEST_PLUGIN__JSON: { type: 'json', auth: 'none', upload: false },
|
|
348
|
+
TEST_PLUGIN__JSON_POST: { type: 'json', auth: 'none', upload: false },
|
|
349
|
+
TEST_PLUGIN__JSON_POST_AUTH: { type: 'json', auth: 'strong', upload: false },
|
|
350
|
+
TEST_PLUGIN__LIST: { type: 'list', auth: 'none', upload: false },
|
|
351
|
+
TEST_PLUGIN__LIST_POST: { type: 'list', auth: 'none', upload: false },
|
|
352
|
+
TEST_PLUGIN__LIST_POST_AUTH: { type: 'list', auth: 'strong', upload: false },
|
|
353
|
+
TEST_PLUGIN__VOID: { type: 'void', auth: 'none', upload: false },
|
|
354
|
+
TEST_PLUGIN__VOID_POST: { type: 'void', auth: 'none', upload: false },
|
|
355
|
+
TEST_PLUGIN__VOID_POST_AUTH: { type: 'void', auth: 'strong', upload: false },
|
|
356
|
+
DESIGN_EDITO_THUMBS_UPLOAD_TEMP: { type: 'json', auth: 'strong', upload: true },
|
|
357
|
+
DESIGN_EDITO_THUMBS_DOWNLOAD_TEMP: { type: 'file', auth: 'strong', upload: false },
|
|
358
|
+
DESIGN_EDITO_THUMBS_TEMPLATE_BOOK_GENERATE: { type: 'file', auth: 'strong', upload: false }
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export { Requester };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Default K-factor used in Elo rating updates. Determines the maximum change in rating per game. */
|
|
2
|
+
export declare const DEFAULT_K_FACTOR = 32;
|
|
3
|
+
/** Default scale used in Elo probability calculation. Represents the rating difference that corresponds to ~90% win probability. */
|
|
4
|
+
export declare const DEFAULT_SCALE = 400;
|
|
5
|
+
/**
|
|
6
|
+
* Calculates the expected probability of player A winning against player B.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} eloA - Elo rating of player A.
|
|
9
|
+
* @param {number} eloB - Elo rating of player B.
|
|
10
|
+
* @param {number} [scale=DEFAULT_SCALE] - Scale factor controlling sensitivity to rating differences.
|
|
11
|
+
* @returns {number} Probability that player A wins (between 0 and 1).
|
|
12
|
+
*/
|
|
13
|
+
export declare function getWinProbability(eloA: number, eloB: number, scale?: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Updates a player's Elo rating based on the expected probability and actual outcome.
|
|
16
|
+
*
|
|
17
|
+
* @param {number} winProbability - Expected probability of winning (from getWinProbability).
|
|
18
|
+
* @param {number} actualOutcome - Actual result of the match (0 = loss, 0.5 = draw, 1 = win; can be fractional for weighted outcomes).
|
|
19
|
+
* @param {number} currentElo - Player's current Elo rating.
|
|
20
|
+
* @param {number} [kFactor=DEFAULT_K_FACTOR] - Maximum rating change per game.
|
|
21
|
+
* @returns {number} Updated Elo rating.
|
|
22
|
+
*/
|
|
23
|
+
export declare function updateRating(winProbability: number, actualOutcome: number, currentElo: number, kFactor?: number): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Default K-factor used in Elo rating updates. Determines the maximum change in rating per game. */
|
|
2
|
+
export const DEFAULT_K_FACTOR = 32;
|
|
3
|
+
/** Default scale used in Elo probability calculation. Represents the rating difference that corresponds to ~90% win probability. */
|
|
4
|
+
export const DEFAULT_SCALE = 400;
|
|
5
|
+
/**
|
|
6
|
+
* Calculates the expected probability of player A winning against player B.
|
|
7
|
+
*
|
|
8
|
+
* @param {number} eloA - Elo rating of player A.
|
|
9
|
+
* @param {number} eloB - Elo rating of player B.
|
|
10
|
+
* @param {number} [scale=DEFAULT_SCALE] - Scale factor controlling sensitivity to rating differences.
|
|
11
|
+
* @returns {number} Probability that player A wins (between 0 and 1).
|
|
12
|
+
*/
|
|
13
|
+
export function getWinProbability(eloA, eloB, scale = DEFAULT_SCALE) {
|
|
14
|
+
const exponent = (eloB - eloA) / scale;
|
|
15
|
+
return 1 / (1 + Math.pow(10, exponent));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Updates a player's Elo rating based on the expected probability and actual outcome.
|
|
19
|
+
*
|
|
20
|
+
* @param {number} winProbability - Expected probability of winning (from getWinProbability).
|
|
21
|
+
* @param {number} actualOutcome - Actual result of the match (0 = loss, 0.5 = draw, 1 = win; can be fractional for weighted outcomes).
|
|
22
|
+
* @param {number} currentElo - Player's current Elo rating.
|
|
23
|
+
* @param {number} [kFactor=DEFAULT_K_FACTOR] - Maximum rating change per game.
|
|
24
|
+
* @returns {number} Updated Elo rating.
|
|
25
|
+
*/
|
|
26
|
+
export function updateRating(winProbability, actualOutcome, currentElo, kFactor = DEFAULT_K_FACTOR) {
|
|
27
|
+
return currentElo + kFactor * (actualOutcome - winProbability);
|
|
28
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DEFAULT_K_FACTOR, DEFAULT_SCALE, getWinProbability, updateRating } from './index.js';
|
|
3
|
+
describe('getWinProbability', () => {
|
|
4
|
+
it('returns 0.5 for equal ratings', () => {
|
|
5
|
+
expect(getWinProbability(1500, 1500)).toBeCloseTo(0.5);
|
|
6
|
+
});
|
|
7
|
+
it('returns higher than 0.5 when first player is stronger', () => {
|
|
8
|
+
expect(getWinProbability(1600, 1400)).toBeGreaterThan(0.5);
|
|
9
|
+
});
|
|
10
|
+
it('returns lower than 0.5 when first player is weaker', () => {
|
|
11
|
+
expect(getWinProbability(1400, 1600)).toBeLessThan(0.5);
|
|
12
|
+
});
|
|
13
|
+
it('respects a custom scale', () => {
|
|
14
|
+
const customScale = 200;
|
|
15
|
+
const p1 = getWinProbability(1600, 1400, customScale);
|
|
16
|
+
const p2 = getWinProbability(1600, 1400, DEFAULT_SCALE);
|
|
17
|
+
expect(p1).toBeGreaterThan(p2); // smaller scale → bigger difference
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('updateRating', () => {
|
|
21
|
+
it('does not change rating if actual equals expected', () => {
|
|
22
|
+
const current = 1500;
|
|
23
|
+
const expected = 0.5;
|
|
24
|
+
const actual = 0.5;
|
|
25
|
+
const newRating = updateRating(expected, actual, current);
|
|
26
|
+
expect(newRating).toBeCloseTo(current);
|
|
27
|
+
});
|
|
28
|
+
it('increases rating if actual > expected', () => {
|
|
29
|
+
const current = 1500;
|
|
30
|
+
const expected = 0.3;
|
|
31
|
+
const actual = 1;
|
|
32
|
+
const newRating = updateRating(expected, actual, current);
|
|
33
|
+
expect(newRating).toBeGreaterThan(current);
|
|
34
|
+
});
|
|
35
|
+
it('decreases rating if actual < expected', () => {
|
|
36
|
+
const current = 1500;
|
|
37
|
+
const expected = 0.7;
|
|
38
|
+
const actual = 0;
|
|
39
|
+
const newRating = updateRating(expected, actual, current);
|
|
40
|
+
expect(newRating).toBeLessThan(current);
|
|
41
|
+
});
|
|
42
|
+
it('accepts fractional actualOutcome (for margin of victory)', () => {
|
|
43
|
+
const current = 1500;
|
|
44
|
+
const expected = 0.4;
|
|
45
|
+
const actual = 0.8;
|
|
46
|
+
const newRating = updateRating(expected, actual, current);
|
|
47
|
+
expect(newRating).toBeGreaterThan(current);
|
|
48
|
+
expect(newRating).toBeLessThan(current + DEFAULT_K_FACTOR); // should not exceed max K
|
|
49
|
+
});
|
|
50
|
+
it('respects custom K-factor', () => {
|
|
51
|
+
const current = 1500;
|
|
52
|
+
const expected = 0.5;
|
|
53
|
+
const actual = 1;
|
|
54
|
+
const customK = 50;
|
|
55
|
+
const newRatingDefault = updateRating(expected, actual, current);
|
|
56
|
+
const newRatingCustom = updateRating(expected, actual, current, customK);
|
|
57
|
+
expect(newRatingCustom - current).toBeGreaterThan(newRatingDefault - current);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('CONSTANTS', () => {
|
|
61
|
+
it('DEFAULT_K_FACTOR is 32', () => {
|
|
62
|
+
expect(DEFAULT_K_FACTOR).toBe(32);
|
|
63
|
+
});
|
|
64
|
+
it('DEFAULT_SCALE is 400', () => {
|
|
65
|
+
expect(DEFAULT_SCALE).toBe(400);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* Linting is suspended on this file — dormant project, left as it was found. See
|
|
2
|
+
« Two files opted out of linting » in README.md before removing this. */
|
|
3
|
+
/* eslint-disable
|
|
4
|
+
@typescript-eslint/no-unsafe-type-assertion,
|
|
5
|
+
@typescript-eslint/no-unused-vars,
|
|
6
|
+
no-console
|
|
7
|
+
*/
|
|
8
|
+
import { shuffle } from '@design-edito/tools/agnostic/arrays/shuffle/index.js';
|
|
9
|
+
/* * * * * * * * * * * * * * * * *
|
|
10
|
+
*
|
|
11
|
+
* Rhyme Generator
|
|
12
|
+
*
|
|
13
|
+
* * * * * * * * * * * * * * * * */
|
|
14
|
+
function getRandomSum(n) {
|
|
15
|
+
if (n <= 0 || !Number.isInteger(n))
|
|
16
|
+
throw new Error('n doit être un entier positif');
|
|
17
|
+
const result = [];
|
|
18
|
+
let remaining = n;
|
|
19
|
+
while (remaining > 0) {
|
|
20
|
+
const value = Math.floor((1 - (Math.random() * Math.random())) * remaining) + 1; // [1, remaining]
|
|
21
|
+
result.push(value);
|
|
22
|
+
remaining -= value;
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
const getWordsByFeet = (feet, wordsRecord) => Object
|
|
27
|
+
.entries(wordsRecord)
|
|
28
|
+
.filter(([, value]) => value === feet)
|
|
29
|
+
.map(([key]) => key);
|
|
30
|
+
const getWords = (feet, wordsRecord, coeff) => {
|
|
31
|
+
const foundWordsThatLong = shuffle(getWordsByFeet(feet, wordsRecord))[0];
|
|
32
|
+
if (feet === 1) {
|
|
33
|
+
if (foundWordsThatLong === undefined)
|
|
34
|
+
throw new Error('need at least 1 word that is 1 foot long');
|
|
35
|
+
return [foundWordsThatLong];
|
|
36
|
+
}
|
|
37
|
+
if (foundWordsThatLong !== undefined
|
|
38
|
+
&& Math.random() < coeff)
|
|
39
|
+
return [foundWordsThatLong];
|
|
40
|
+
let randomSubParts = [];
|
|
41
|
+
while (randomSubParts.length < 2) {
|
|
42
|
+
randomSubParts = getRandomSum(feet);
|
|
43
|
+
}
|
|
44
|
+
return randomSubParts.map(ft => getWords(ft, wordsRecord, coeff)).flat();
|
|
45
|
+
};
|
|
46
|
+
const flattenRhymeRecord = (rhymRec) => {
|
|
47
|
+
return Object.entries(rhymRec).reduce((acc, [rhyme, words]) => {
|
|
48
|
+
return { ...acc, ...words };
|
|
49
|
+
}, {});
|
|
50
|
+
};
|
|
51
|
+
const getRhyme = (suffix, feet, rhymeRecord, coeff) => {
|
|
52
|
+
const foundRhymeThatLong = shuffle(getWordsByFeet(feet, rhymeRecord[suffix] ?? {}))[0];
|
|
53
|
+
if (feet === 1) {
|
|
54
|
+
if (foundRhymeThatLong === undefined)
|
|
55
|
+
return [`${suffix} *`]; //throw new Error('need at least 1 word that is 1 foot long')
|
|
56
|
+
return [foundRhymeThatLong];
|
|
57
|
+
}
|
|
58
|
+
if (foundRhymeThatLong !== undefined
|
|
59
|
+
&& Math.random() < coeff)
|
|
60
|
+
return [foundRhymeThatLong];
|
|
61
|
+
let randomSubParts = [];
|
|
62
|
+
while (randomSubParts.length < 2) {
|
|
63
|
+
randomSubParts = getRandomSum(feet);
|
|
64
|
+
}
|
|
65
|
+
return randomSubParts.map((ft, pos) => {
|
|
66
|
+
if (pos === randomSubParts.length - 1)
|
|
67
|
+
return getRhyme(suffix, ft, rhymeRecord, coeff);
|
|
68
|
+
return shuffle(getWords(ft, flattenRhymeRecord(rhymeRecord), coeff));
|
|
69
|
+
}).flat();
|
|
70
|
+
};
|
|
71
|
+
const getPoem = function (stanza, rhymeRecord, coeff) {
|
|
72
|
+
return stanza.map(({ length, rhyme }) => length === 0 ? '' : getRhyme(rhyme, length, rhymeRecord, coeff).join(' ')).join('\n');
|
|
73
|
+
};
|
|
74
|
+
/* * * * * * * * * * * * * * * * *
|
|
75
|
+
*
|
|
76
|
+
* TESTS - Rhyme Generator
|
|
77
|
+
*
|
|
78
|
+
* * * * * * * * * * * * * * * * */
|
|
79
|
+
const rhymeRecord = {
|
|
80
|
+
'-a': {
|
|
81
|
+
'a': 1,
|
|
82
|
+
},
|
|
83
|
+
'-è': {
|
|
84
|
+
'eh': 1,
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const _rhymeRecordTypecheck = rhymeRecord;
|
|
88
|
+
const COEFF = .85;
|
|
89
|
+
console.log(`
|
|
90
|
+
|
|
91
|
+
${getPoem([{
|
|
92
|
+
length: 4,
|
|
93
|
+
rhyme: '-a'
|
|
94
|
+
}, {
|
|
95
|
+
length: 4,
|
|
96
|
+
rhyme: '-a'
|
|
97
|
+
}, {
|
|
98
|
+
length: 3,
|
|
99
|
+
rhyme: '-è'
|
|
100
|
+
}, {
|
|
101
|
+
length: 0
|
|
102
|
+
}, {
|
|
103
|
+
length: 2,
|
|
104
|
+
rhyme: '-a'
|
|
105
|
+
}, {
|
|
106
|
+
length: 0
|
|
107
|
+
}, {
|
|
108
|
+
length: 2,
|
|
109
|
+
rhyme: '-è'
|
|
110
|
+
}, {
|
|
111
|
+
length: 4,
|
|
112
|
+
rhyme: '-è'
|
|
113
|
+
}, {
|
|
114
|
+
length: 0
|
|
115
|
+
}, {
|
|
116
|
+
length: 4,
|
|
117
|
+
rhyme: '-a'
|
|
118
|
+
}, {
|
|
119
|
+
length: 3,
|
|
120
|
+
rhyme: '-è'
|
|
121
|
+
}, {
|
|
122
|
+
length: 1542,
|
|
123
|
+
rhyme: '-a'
|
|
124
|
+
}], rhymeRecord, COEFF)}
|
|
125
|
+
`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as generateRhymes from './generate-rhymes/index.js'
|