@gaurav_bhandari/common-frontend-services 1.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/README.md +40 -0
- package/dist/bin/cli.js +22 -0
- package/dist/src/cli/commands/add.js +104 -0
- package/dist/src/cli/commands/list.js +19 -0
- package/dist/src/cli/registry.js +125 -0
- package/package.json +39 -0
- package/src/services/API-Service/baseApiService.ts +296 -0
- package/src/services/API-Service/csrfAPIService.ts +93 -0
- package/src/services/API-Service/docs/WhatIsBaseApiService.md +121 -0
- package/src/services/API-Service/docs/WhatIsCsrfApiService.md +67 -0
- package/src/services/Architecture-Reports-Service/architecture-checklist.md +54 -0
- package/src/services/Architecture-Reports-Service/report-generation-prompt.md +60 -0
- package/src/services/Authorization-Service/docs/WhatIsRBACService.md +112 -0
- package/src/services/Authorization-Service/permissions.ts +16 -0
- package/src/services/Authorization-Service/rbacService.ts +63 -0
- package/src/services/Authorization-Service/rolePermissions.ts +10 -0
- package/src/services/Authorization-Service/userPermissions/adminPermissions.ts +13 -0
- package/src/services/Cache-Service/README.md +50 -0
- package/src/services/Cache-Service/cacheService.ts +142 -0
- package/src/services/Cache-Service/simple-explanation.md +55 -0
- package/src/services/Cache-Service/sw-strategies.ts +51 -0
- package/src/services/Date-Service/dateService.ts +93 -0
- package/src/services/Date-Service/docs/WhatIsDateService.md +63 -0
- package/src/services/Environment-Config-Service/docs/WhatIsEnvironmentConfigService.md +68 -0
- package/src/services/Environment-Config-Service/environmentConfigService.ts +112 -0
- package/src/services/Environment-Config-Service/simple-explanation.md +49 -0
- package/src/services/Feature-Flag-Service/docs/WhatIsFeatureFlagService.md +98 -0
- package/src/services/Feature-Flag-Service/featureFlagService.ts +91 -0
- package/src/services/File-Service/docs/WhatIsFileService.md +78 -0
- package/src/services/File-Service/fileService.ts +224 -0
- package/src/services/File-Service/simple-explanation.md +49 -0
- package/src/services/Google-Login-Service/README.md +48 -0
- package/src/services/Google-Login-Service/googleLoginService.ts +209 -0
- package/src/services/GraphQl-Service/docs/WhatIsGraphQLService.md +101 -0
- package/src/services/GraphQl-Service/graphqlService.ts +81 -0
- package/src/services/Logger-Service/README.md +44 -0
- package/src/services/Logger-Service/loggerService.ts +125 -0
- package/src/services/Logger-Service/simple-explanation.md +43 -0
- package/src/services/Logger-Service/verify-logger.ts +62 -0
- package/src/services/Monitoring-Service/README.md +48 -0
- package/src/services/Monitoring-Service/monitoringService.ts +229 -0
- package/src/services/Monitoring-Service/simple-explanation.md +48 -0
- package/src/services/Monitoring-Service/verify-monitoring.ts +74 -0
- package/src/services/Security-Reports-Service/report-generation-prompt.md +52 -0
- package/src/services/Security-Reports-Service/security-checklist.md +66 -0
- package/src/services/Storage-Service/docs/WhatIsStorageService.md +70 -0
- package/src/services/Storage-Service/indexedDBService.ts +103 -0
- package/src/services/Storage-Service/localStorageService.ts +42 -0
- package/src/services/Storage-Service/sessionStorageService.ts +42 -0
- package/src/services/Validation-Service/form.ts +66 -0
- package/src/services/Validation-Service/rules.ts +338 -0
- package/src/services/Worker-Service/docs/WhatIsWorkerService.md +57 -0
- package/src/services/Worker-Service/workerService.ts +122 -0
- package/src/services/i18n-Service/docs/WhatIsI18nService.md +108 -0
- package/src/services/i18n-Service/i18nService.ts +127 -0
- package/src/services/i18n-Service/locales/en.json +17 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class LocalStorageService {
|
|
2
|
+
private static instance: LocalStorageService;
|
|
3
|
+
|
|
4
|
+
private constructor() {}
|
|
5
|
+
|
|
6
|
+
public static getInstance(): LocalStorageService {
|
|
7
|
+
if (!LocalStorageService.instance) {
|
|
8
|
+
LocalStorageService.instance = new LocalStorageService();
|
|
9
|
+
}
|
|
10
|
+
return LocalStorageService.instance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public setItem<T>(key: string, value: T): void {
|
|
14
|
+
try {
|
|
15
|
+
const serialized = JSON.stringify(value);
|
|
16
|
+
localStorage.setItem(key, serialized);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("LocalStorage setItem failed:", error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getItem<T>(key: string, defaultValue: T | null = null): T | null {
|
|
23
|
+
try {
|
|
24
|
+
const serialized = localStorage.getItem(key);
|
|
25
|
+
if (!serialized) return defaultValue;
|
|
26
|
+
return JSON.parse(serialized) as T;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("LocalStorage getItem failed:", error);
|
|
29
|
+
return defaultValue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public removeItem(key: string): void {
|
|
34
|
+
localStorage.removeItem(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public clear(): void {
|
|
38
|
+
localStorage.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const localStorageService = LocalStorageService.getInstance();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class SessionStorageService {
|
|
2
|
+
private static instance: SessionStorageService;
|
|
3
|
+
|
|
4
|
+
private constructor() {}
|
|
5
|
+
|
|
6
|
+
public static getInstance(): SessionStorageService {
|
|
7
|
+
if (!SessionStorageService.instance) {
|
|
8
|
+
SessionStorageService.instance = new SessionStorageService();
|
|
9
|
+
}
|
|
10
|
+
return SessionStorageService.instance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public setItem<T>(key: string, value: T): void {
|
|
14
|
+
try {
|
|
15
|
+
const serialized = JSON.stringify(value);
|
|
16
|
+
sessionStorage.setItem(key, serialized);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("SessionStorage setItem failed:", error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getItem<T>(key: string, defaultValue: T | null = null): T | null {
|
|
23
|
+
try {
|
|
24
|
+
const serialized = sessionStorage.getItem(key);
|
|
25
|
+
if (!serialized) return defaultValue;
|
|
26
|
+
return JSON.parse(serialized) as T;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("SessionStorage getItem failed:", error);
|
|
29
|
+
return defaultValue;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public removeItem(key: string): void {
|
|
34
|
+
sessionStorage.removeItem(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public clear(): void {
|
|
38
|
+
sessionStorage.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const sessionStorageService = SessionStorageService.getInstance();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { RuleFn } from "./rules";
|
|
2
|
+
|
|
3
|
+
export interface FieldConfig<T = any> {
|
|
4
|
+
rules: RuleFn<T>[];
|
|
5
|
+
value?: T;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
isValid: boolean;
|
|
10
|
+
errors: Record<string, string[]>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class FormValidator<T extends Record<string, any>> {
|
|
14
|
+
private schema: Record<keyof T, RuleFn[]>;
|
|
15
|
+
|
|
16
|
+
constructor(schema: Record<keyof T, RuleFn[]>) {
|
|
17
|
+
this.schema = schema;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a full data object against the schema
|
|
22
|
+
*/
|
|
23
|
+
public validate(data: T): ValidationResult {
|
|
24
|
+
const errors: Record<string, string[]> = {};
|
|
25
|
+
let isValid = true;
|
|
26
|
+
|
|
27
|
+
for (const key in this.schema) {
|
|
28
|
+
const fieldRules = this.schema[key];
|
|
29
|
+
const value = data[key];
|
|
30
|
+
const fieldErrors: string[] = [];
|
|
31
|
+
|
|
32
|
+
for (const rule of fieldRules) {
|
|
33
|
+
const result = rule(value);
|
|
34
|
+
if (result !== true) {
|
|
35
|
+
fieldErrors.push(
|
|
36
|
+
typeof result === "string" ? result : "Invalid value",
|
|
37
|
+
);
|
|
38
|
+
isValid = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (fieldErrors.length > 0) {
|
|
43
|
+
errors[key] = fieldErrors;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { isValid, errors };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate a single field
|
|
52
|
+
*/
|
|
53
|
+
public validateField(key: keyof T, value: any): string[] {
|
|
54
|
+
const rules = this.schema[key] || [];
|
|
55
|
+
const errors: string[] = [];
|
|
56
|
+
|
|
57
|
+
for (const rule of rules) {
|
|
58
|
+
const result = rule(value);
|
|
59
|
+
if (result !== true) {
|
|
60
|
+
errors.push(typeof result === "string" ? result : "Invalid value");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return errors;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// All rules return true or a string (error message) to plug directly into v-text-field `rules` prop
|
|
2
|
+
|
|
3
|
+
export type RuleFn<T = any> = (value: T) => true | string;
|
|
4
|
+
|
|
5
|
+
const isEmpty = (v: unknown) =>
|
|
6
|
+
v === null ||
|
|
7
|
+
v === undefined ||
|
|
8
|
+
(typeof v === "string" && v.trim() === "") ||
|
|
9
|
+
(Array.isArray(v) && v.length === 0);
|
|
10
|
+
|
|
11
|
+
export const required =
|
|
12
|
+
(msg = "This field is required"): RuleFn =>
|
|
13
|
+
(value) => {
|
|
14
|
+
return !isEmpty(value) || msg;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const email =
|
|
18
|
+
(msg = "Please enter a valid email"): RuleFn<string> =>
|
|
19
|
+
(value) => {
|
|
20
|
+
if (isEmpty(value)) return true; // allow empty unless required is also used
|
|
21
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value).trim()) || msg;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// At least 6 chars, 1 uppercase, 1 lowercase, 1 number, 1 special character
|
|
25
|
+
export const password =
|
|
26
|
+
(
|
|
27
|
+
msg = "Password must be at least 6 characters long and include uppercase, lowercase, numbers and a special character",
|
|
28
|
+
): RuleFn<string> =>
|
|
29
|
+
(value) => {
|
|
30
|
+
if (isEmpty(value)) return true;
|
|
31
|
+
const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9\s])[^\s]{6,}$/;
|
|
32
|
+
return re.test(String(value).trim()) || msg;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// check for leading or trailing spaces
|
|
36
|
+
export const noLeadingTrailingSpaces =
|
|
37
|
+
(msg = "No leading or trailing spaces allowed"): RuleFn<string> =>
|
|
38
|
+
(value) => {
|
|
39
|
+
if (isEmpty(value)) return true;
|
|
40
|
+
const str = String(value);
|
|
41
|
+
return str === str.trim() || msg;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// international phone (10+ digits)
|
|
45
|
+
export const phone =
|
|
46
|
+
(msg = "Please enter a valid phone number"): RuleFn<string> =>
|
|
47
|
+
(value) => {
|
|
48
|
+
if (isEmpty(value)) return true;
|
|
49
|
+
return /^\d{10,15}$/.test(String(value).trim()) || msg;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Builders for custom needs
|
|
53
|
+
export const minLength =
|
|
54
|
+
(len: number, msg?: string): RuleFn<string> =>
|
|
55
|
+
(value) => {
|
|
56
|
+
if (isEmpty(value)) return true;
|
|
57
|
+
return (
|
|
58
|
+
String(value).length >= len || msg || `Must be at least ${len} characters`
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const maxLength =
|
|
63
|
+
(len: number, msg?: string): RuleFn<string> =>
|
|
64
|
+
(value) => {
|
|
65
|
+
if (isEmpty(value)) return true;
|
|
66
|
+
return (
|
|
67
|
+
String(value).length <= len || msg || `Must be at most ${len} characters`
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const pattern =
|
|
72
|
+
(re: RegExp, msg = "Invalid value"): RuleFn<string> =>
|
|
73
|
+
(value) => {
|
|
74
|
+
if (isEmpty(value)) return true;
|
|
75
|
+
return re.test(String(value)) || msg;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const equals =
|
|
79
|
+
(
|
|
80
|
+
other: () => string | number | boolean | null | undefined,
|
|
81
|
+
msg = "Values do not match",
|
|
82
|
+
): RuleFn<any> =>
|
|
83
|
+
(value) => {
|
|
84
|
+
return value === other() || msg;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const notEquals =
|
|
88
|
+
(
|
|
89
|
+
other: () => string | number | boolean | null | undefined,
|
|
90
|
+
msg = "Values must not match",
|
|
91
|
+
): RuleFn<any> =>
|
|
92
|
+
(value) => {
|
|
93
|
+
return value !== other() || msg;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// File-specific validation rules
|
|
97
|
+
export const fileSize =
|
|
98
|
+
(maxBytes: number, msg?: string): RuleFn<File> =>
|
|
99
|
+
(file) => {
|
|
100
|
+
if (!file) return true;
|
|
101
|
+
const maxMB = Math.round((maxBytes / (1024 * 1024)) * 10) / 10;
|
|
102
|
+
return (
|
|
103
|
+
file.size <= maxBytes || msg || `File size must be less than ${maxMB}MB`
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const minFileSize =
|
|
108
|
+
(minBytes: number, msg?: string): RuleFn<File> =>
|
|
109
|
+
(file) => {
|
|
110
|
+
if (!file) return true;
|
|
111
|
+
const minMB = Math.round((minBytes / (1024 * 1024)) * 10) / 10;
|
|
112
|
+
return (
|
|
113
|
+
file.size >= minBytes || msg || `File size must be at least ${minMB}MB`
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const fileType =
|
|
118
|
+
(allowedTypes: string[], msg?: string): RuleFn<File> =>
|
|
119
|
+
(file) => {
|
|
120
|
+
if (!file) return true;
|
|
121
|
+
return (
|
|
122
|
+
allowedTypes.includes(file.type) ||
|
|
123
|
+
msg ||
|
|
124
|
+
`File type ${file.type} is not allowed`
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const fileExtension =
|
|
129
|
+
(allowedExtensions: string[], msg?: string): RuleFn<File> =>
|
|
130
|
+
(file) => {
|
|
131
|
+
if (!file) return true;
|
|
132
|
+
const fileExt = "." + file.name.split(".").pop()?.toLowerCase();
|
|
133
|
+
const allowedExtsLower = allowedExtensions.map((ext) => ext.toLowerCase());
|
|
134
|
+
return (
|
|
135
|
+
allowedExtsLower.includes(fileExt) ||
|
|
136
|
+
msg ||
|
|
137
|
+
`File extension ${fileExt} is not allowed`
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const imageFile =
|
|
142
|
+
(msg = "Please select a valid image file"): RuleFn<File> =>
|
|
143
|
+
(file) => {
|
|
144
|
+
if (!file) return true;
|
|
145
|
+
return file.type.startsWith("image/") || msg;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const pdfFile =
|
|
149
|
+
(msg = "Please select a valid PDF file"): RuleFn<File> =>
|
|
150
|
+
(file) => {
|
|
151
|
+
if (!file) return true;
|
|
152
|
+
return file.type === "application/pdf" || msg;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Helper to combine multiple rules into one rule function (short-circuits on first error)
|
|
156
|
+
export const composeRules =
|
|
157
|
+
(...rules: RuleFn[]): RuleFn =>
|
|
158
|
+
(value) => {
|
|
159
|
+
for (const rule of rules) {
|
|
160
|
+
const res = rule(value);
|
|
161
|
+
if (res !== true) return res;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export const numericValue =
|
|
167
|
+
(msg = "Please enter a valid number"): RuleFn<string> =>
|
|
168
|
+
(value) => {
|
|
169
|
+
if (isEmpty(value)) return true;
|
|
170
|
+
const numberRegex = /^\d*$/;
|
|
171
|
+
return numberRegex.test(String(value)) || msg;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Date range validation rules
|
|
175
|
+
export const dateRange =
|
|
176
|
+
(
|
|
177
|
+
_msg = "Please select a valid date range",
|
|
178
|
+
): RuleFn<{ from: string | null; to: string | null }> =>
|
|
179
|
+
(value) => {
|
|
180
|
+
if (!value || (isEmpty(value.from) && isEmpty(value.to))) return true;
|
|
181
|
+
|
|
182
|
+
// If only one date is selected, both should be selected
|
|
183
|
+
if ((value.from && !value.to) || (!value.from && value.to)) {
|
|
184
|
+
return "Please select both from and to dates";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// If both dates are selected, validate the range
|
|
188
|
+
if (value.from && value.to) {
|
|
189
|
+
const fromDate = new Date(value.from);
|
|
190
|
+
const toDate = new Date(value.to);
|
|
191
|
+
|
|
192
|
+
if (fromDate >= toDate) {
|
|
193
|
+
return "From date must be before to date";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return true;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const dateRangeRequired =
|
|
201
|
+
(
|
|
202
|
+
msg = "Date range is required",
|
|
203
|
+
): RuleFn<{ from: string | null; to: string | null }> =>
|
|
204
|
+
(value) => {
|
|
205
|
+
if (!value || isEmpty(value.from) || isEmpty(value.to)) {
|
|
206
|
+
return msg;
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const maxDateRange =
|
|
212
|
+
(
|
|
213
|
+
maxDays: number,
|
|
214
|
+
msg?: string,
|
|
215
|
+
): RuleFn<{ from: string | null; to: string | null }> =>
|
|
216
|
+
(value) => {
|
|
217
|
+
if (!value || isEmpty(value.from) || isEmpty(value.to)) return true;
|
|
218
|
+
|
|
219
|
+
const fromDate = new Date(value.from as string);
|
|
220
|
+
const toDate = new Date(value.to as string);
|
|
221
|
+
const diffTime = toDate.getTime() - fromDate.getTime();
|
|
222
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
diffDays <= maxDays || msg || `Date range cannot exceed ${maxDays} days`
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
export const positiveNumber =
|
|
230
|
+
(msg = "Please enter a positive number"): RuleFn<string | number> =>
|
|
231
|
+
(value) => {
|
|
232
|
+
if (isEmpty(value)) return true;
|
|
233
|
+
const num =
|
|
234
|
+
typeof value === "number"
|
|
235
|
+
? value
|
|
236
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
237
|
+
return (!isNaN(num) && num > 0) || msg;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const nonNegativeNumber =
|
|
241
|
+
(msg = "Please enter a non-negative number"): RuleFn<string | number> =>
|
|
242
|
+
(value) => {
|
|
243
|
+
if (isEmpty(value)) return true;
|
|
244
|
+
const num =
|
|
245
|
+
typeof value === "number"
|
|
246
|
+
? value
|
|
247
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
248
|
+
return (!isNaN(num) && num >= 0) || msg;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const integerValue =
|
|
252
|
+
(msg = "Please enter a whole number"): RuleFn<string | number> =>
|
|
253
|
+
(value) => {
|
|
254
|
+
if (isEmpty(value)) return true;
|
|
255
|
+
const num =
|
|
256
|
+
typeof value === "number"
|
|
257
|
+
? value
|
|
258
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
259
|
+
return (!isNaN(num) && Number.isInteger(num)) || msg;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const minValue =
|
|
263
|
+
(min: number, msg?: string): RuleFn<string | number> =>
|
|
264
|
+
(value) => {
|
|
265
|
+
if (isEmpty(value)) return true;
|
|
266
|
+
const num =
|
|
267
|
+
typeof value === "number"
|
|
268
|
+
? value
|
|
269
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
270
|
+
return (
|
|
271
|
+
(!isNaN(num) && num >= min) || msg || `Value must be at least ${min}`
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const maxValue =
|
|
276
|
+
(max: number, msg?: string): RuleFn<string | number> =>
|
|
277
|
+
(value) => {
|
|
278
|
+
if (isEmpty(value)) return true;
|
|
279
|
+
const num =
|
|
280
|
+
typeof value === "number"
|
|
281
|
+
? value
|
|
282
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
283
|
+
return (!isNaN(num) && num <= max) || msg || `Value must be at most ${max}`;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const halfStepNumber =
|
|
287
|
+
(msg = "Please enter a number in 0.5 increments"): RuleFn<string | number> =>
|
|
288
|
+
(value) => {
|
|
289
|
+
if (isEmpty(value)) return true;
|
|
290
|
+
const num =
|
|
291
|
+
typeof value === "number"
|
|
292
|
+
? value
|
|
293
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
294
|
+
return (!isNaN(num) && (num * 2) % 1 === 0) || msg;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
export const isNumber =
|
|
298
|
+
(msg = "Please enter a valid number"): RuleFn<string | number> =>
|
|
299
|
+
(value) => {
|
|
300
|
+
if (isEmpty(value)) return true;
|
|
301
|
+
const num =
|
|
302
|
+
typeof value === "number"
|
|
303
|
+
? value
|
|
304
|
+
: parseFloat(String(value).replace(/,/g, ""));
|
|
305
|
+
return !isNaN(num) || msg;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const Rules = {
|
|
309
|
+
required,
|
|
310
|
+
email,
|
|
311
|
+
password,
|
|
312
|
+
noLeadingTrailingSpaces,
|
|
313
|
+
phone,
|
|
314
|
+
minLength,
|
|
315
|
+
maxLength,
|
|
316
|
+
pattern,
|
|
317
|
+
equals,
|
|
318
|
+
composeRules,
|
|
319
|
+
numericValue,
|
|
320
|
+
dateRange,
|
|
321
|
+
dateRangeRequired,
|
|
322
|
+
maxDateRange,
|
|
323
|
+
positiveNumber,
|
|
324
|
+
nonNegativeNumber,
|
|
325
|
+
integerValue,
|
|
326
|
+
minValue,
|
|
327
|
+
maxValue,
|
|
328
|
+
halfStepNumber,
|
|
329
|
+
isNumber,
|
|
330
|
+
fileSize,
|
|
331
|
+
minFileSize,
|
|
332
|
+
fileType,
|
|
333
|
+
fileExtension,
|
|
334
|
+
imageFile,
|
|
335
|
+
pdfFile,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export type { RuleFn as ValidationRuleFn };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# WorkerService
|
|
2
|
+
|
|
3
|
+
The `WorkerService` allows you to offload heavy computations to a background thread without creating separate worker files. It creates workers dynamically using Blob URLs.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Use this service when you have CPU-intensive tasks that would otherwise block the main thread and freeze the UI (e.g., sorting large arrays, image processing, complex filtering).
|
|
8
|
+
|
|
9
|
+
### Key Features
|
|
10
|
+
|
|
11
|
+
- **Dynamic Execution**: Pass a function and data, get a Promise back.
|
|
12
|
+
- **No Setup**: No need to configure webpack/vite worker loaders for simple tasks.
|
|
13
|
+
- **Type Safety**: Generic support for input and output types.
|
|
14
|
+
|
|
15
|
+
## How to Use
|
|
16
|
+
|
|
17
|
+
### 1. One-off Execution
|
|
18
|
+
|
|
19
|
+
Execute a function in a worker and get the result.
|
|
20
|
+
|
|
21
|
+
**⚠️ IMPORTANT LIMITATION**: The function passed to `execute` MUST be pure and self-contained. It **cannot** access variables from the outer scope (closures) because it is serialized to a string and run in a separate context.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { workerService } from "@/Worker-Service/workerService";
|
|
25
|
+
|
|
26
|
+
// Example: Sorting a large array
|
|
27
|
+
const largeArray = [
|
|
28
|
+
/* ... 100k items ... */
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const sorted = await workerService.execute((data) => {
|
|
32
|
+
// This code runs in the worker
|
|
33
|
+
return data.sort((a, b) => a - b);
|
|
34
|
+
}, largeArray);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Long-lived Worker
|
|
38
|
+
|
|
39
|
+
If you need a worker that stays alive (e.g., for processing a stream of data), use `createWorker`.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const worker = workerService.createWorker(() => {
|
|
43
|
+
self.onmessage = (e) => {
|
|
44
|
+
const result = e.data * 2;
|
|
45
|
+
self.postMessage(result);
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
worker.onmessage = (e) => console.log(e.data);
|
|
50
|
+
worker.postMessage(10); // Logs: 20
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Best Practices
|
|
54
|
+
|
|
55
|
+
1. **Pure Functions**: Ensure your worker function relies _only_ on its arguments.
|
|
56
|
+
2. **Data Transfer**: Data passed to workers is cloned (structured clone algorithm). Avoid passing non-cloneable objects (like DOM elements or functions).
|
|
57
|
+
3. **Error Handling**: The `execute` method wraps errors, so use standard `try/catch`.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export interface WorkerOptions {
|
|
2
|
+
timeout?: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class WorkerService {
|
|
6
|
+
private static instance: WorkerService;
|
|
7
|
+
|
|
8
|
+
private constructor() {}
|
|
9
|
+
|
|
10
|
+
public static getInstance(): WorkerService {
|
|
11
|
+
if (!WorkerService.instance) {
|
|
12
|
+
WorkerService.instance = new WorkerService();
|
|
13
|
+
}
|
|
14
|
+
return WorkerService.instance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Execute a function in a web worker.
|
|
19
|
+
* NOTE: The function `fn` must be strictly self-contained (no external closures).
|
|
20
|
+
*
|
|
21
|
+
* @param fn The function to execute.
|
|
22
|
+
* @param data The data to pass to the function.
|
|
23
|
+
* @param options Execution options (e.g., timeout).
|
|
24
|
+
*/
|
|
25
|
+
public execute<T, R>(
|
|
26
|
+
fn: (data: T) => R,
|
|
27
|
+
data: T,
|
|
28
|
+
options?: WorkerOptions,
|
|
29
|
+
): Promise<R> {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const code = `
|
|
32
|
+
self.onmessage = function(e) {
|
|
33
|
+
try {
|
|
34
|
+
const fn = ${fn.toString()};
|
|
35
|
+
const result = fn(e.data);
|
|
36
|
+
self.postMessage({ result, error: null });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
self.postMessage({ result: null, error: error instanceof Error ? error.message : String(error) });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const blob = new Blob([code], { type: "application/javascript" });
|
|
44
|
+
const url = URL.createObjectURL(blob);
|
|
45
|
+
const worker = new Worker(url);
|
|
46
|
+
let timeoutId: any;
|
|
47
|
+
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
50
|
+
worker.terminate();
|
|
51
|
+
URL.revokeObjectURL(url);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
worker.onmessage = (e) => {
|
|
55
|
+
const { result, error } = e.data;
|
|
56
|
+
cleanup();
|
|
57
|
+
if (error) {
|
|
58
|
+
reject(new Error(`Worker execution failed: ${error}`));
|
|
59
|
+
} else {
|
|
60
|
+
resolve(result);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
worker.onerror = (e) => {
|
|
65
|
+
cleanup();
|
|
66
|
+
reject(new Error(`Worker error: ${e.message}`));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (options?.timeout) {
|
|
70
|
+
timeoutId = setTimeout(() => {
|
|
71
|
+
cleanup();
|
|
72
|
+
reject(new Error(`Worker timed out after ${options.timeout}ms`));
|
|
73
|
+
}, options.timeout);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
worker.postMessage(data);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Run a worker from a script file URL.
|
|
82
|
+
* @param scriptUrl URL of the worker script.
|
|
83
|
+
* @param data Data to pass.
|
|
84
|
+
* @param options Execution options.
|
|
85
|
+
*/
|
|
86
|
+
public runWorker<T, R>(
|
|
87
|
+
scriptUrl: string,
|
|
88
|
+
data: T,
|
|
89
|
+
options?: WorkerOptions,
|
|
90
|
+
): Promise<R> {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const worker = new Worker(scriptUrl);
|
|
93
|
+
let timeoutId: any;
|
|
94
|
+
|
|
95
|
+
const cleanup = () => {
|
|
96
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
97
|
+
worker.terminate();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
worker.onmessage = (e) => {
|
|
101
|
+
cleanup();
|
|
102
|
+
resolve(e.data as R);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
worker.onerror = (e) => {
|
|
106
|
+
cleanup();
|
|
107
|
+
reject(new Error(`Worker error: ${e.message}`));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (options?.timeout) {
|
|
111
|
+
timeoutId = setTimeout(() => {
|
|
112
|
+
cleanup();
|
|
113
|
+
reject(new Error(`Worker timed out after ${options.timeout}ms`));
|
|
114
|
+
}, options.timeout);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
worker.postMessage(data);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const workerService = WorkerService.getInstance();
|