@bonsae/nrg 0.1.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 +130 -0
- package/build/server/index.cjs +910 -0
- package/build/server/resources/nrg-client.js +6530 -0
- package/build/server/resources/vue.esm-browser.prod.js +13 -0
- package/build/vite/index.js +1893 -0
- package/build/vite/utils.js +60 -0
- package/package.json +110 -0
- package/src/core/client/api/index.ts +17 -0
- package/src/core/client/app.vue +201 -0
- package/src/core/client/components/node-red-config-input.vue +57 -0
- package/src/core/client/components/node-red-editor-input.vue +283 -0
- package/src/core/client/components/node-red-input.vue +71 -0
- package/src/core/client/components/node-red-json-schema-form.vue +369 -0
- package/src/core/client/components/node-red-select-input.vue +86 -0
- package/src/core/client/components/node-red-typed-input.vue +130 -0
- package/src/core/client/components.d.ts +18 -0
- package/src/core/client/globals.d.ts +17 -0
- package/src/core/client/index.ts +504 -0
- package/src/core/client/shims-vue.d.ts +5 -0
- package/src/core/client/tsconfig.json +18 -0
- package/src/core/client/virtual.d.ts +5 -0
- package/src/core/constants.ts +18 -0
- package/src/core/server/index.ts +209 -0
- package/src/core/server/nodes/config-node.ts +67 -0
- package/src/core/server/nodes/index.ts +4 -0
- package/src/core/server/nodes/io-node.ts +178 -0
- package/src/core/server/nodes/node.ts +255 -0
- package/src/core/server/nodes/types/config-node.ts +28 -0
- package/src/core/server/nodes/types/index.ts +3 -0
- package/src/core/server/nodes/types/io-node.ts +37 -0
- package/src/core/server/nodes/types/node.ts +41 -0
- package/src/core/server/nodes/utils.ts +83 -0
- package/src/core/server/schemas/base.ts +66 -0
- package/src/core/server/schemas/index.ts +3 -0
- package/src/core/server/schemas/type.ts +95 -0
- package/src/core/server/schemas/types/index.ts +73 -0
- package/src/core/server/tsconfig.json +17 -0
- package/src/core/server/types/index.ts +73 -0
- package/src/core/server/utils.ts +56 -0
- package/src/core/server/validator.ts +32 -0
- package/src/core/validator.ts +222 -0
- package/src/tsconfig/base.json +23 -0
- package/src/tsconfig/client.json +11 -0
- package/src/tsconfig/server.json +6 -0
- package/src/vite/async-utils.ts +61 -0
- package/src/vite/client/build.ts +223 -0
- package/src/vite/client/index.ts +1 -0
- package/src/vite/client/plugins/html-generator.ts +75 -0
- package/src/vite/client/plugins/index.ts +5 -0
- package/src/vite/client/plugins/locales-generator.ts +126 -0
- package/src/vite/client/plugins/minifier.ts +22 -0
- package/src/vite/client/plugins/node-definitions-inliner.ts +224 -0
- package/src/vite/client/plugins/static-copy.ts +43 -0
- package/src/vite/defaults.ts +77 -0
- package/src/vite/errors.ts +37 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/logger.ts +94 -0
- package/src/vite/node-red-launcher.ts +344 -0
- package/src/vite/plugin.ts +61 -0
- package/src/vite/plugins/build.ts +73 -0
- package/src/vite/plugins/index.ts +2 -0
- package/src/vite/plugins/server.ts +267 -0
- package/src/vite/server/build.ts +124 -0
- package/src/vite/server/index.ts +1 -0
- package/src/vite/server/plugins/index.ts +3 -0
- package/src/vite/server/plugins/output-wrapper.ts +109 -0
- package/src/vite/server/plugins/package-json-generator.ts +203 -0
- package/src/vite/server/plugins/type-generator.ts +285 -0
- package/src/vite/types.ts +369 -0
- package/src/vite/utils.ts +103 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"noImplicitAny": false,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts", "../constants.ts", "../validator.ts"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { TObject } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
interface RED {
|
|
4
|
+
_: (key: string, substitutions?: Record<string, string>) => string;
|
|
5
|
+
log: {
|
|
6
|
+
info(msg: any): void;
|
|
7
|
+
warn(msg: any): void;
|
|
8
|
+
error(message: string, error: any): void;
|
|
9
|
+
debug(msg: any): void;
|
|
10
|
+
trace(msg: any): void;
|
|
11
|
+
};
|
|
12
|
+
nodes: {
|
|
13
|
+
registerType: (type: string, def: any, opts?: any) => void;
|
|
14
|
+
getNode: <T = any>(id: string) => T | undefined;
|
|
15
|
+
createNode: (node: any, config: Record<string, any>) => void;
|
|
16
|
+
getCredentials: (id: string) => Record<string, any> | undefined;
|
|
17
|
+
};
|
|
18
|
+
httpAdmin: {
|
|
19
|
+
get(path: string, handler: (req: any, res: any) => void): void;
|
|
20
|
+
post(path: string, handler: (req: any, res: any) => void): void;
|
|
21
|
+
put(path: string, handler: (req: any, res: any) => void): void;
|
|
22
|
+
delete(path: string, handler: (req: any, res: any) => void): void;
|
|
23
|
+
};
|
|
24
|
+
util: {
|
|
25
|
+
evaluateNodeProperty(
|
|
26
|
+
value: any,
|
|
27
|
+
type: string,
|
|
28
|
+
node: any,
|
|
29
|
+
msg: Record<string, any> | undefined,
|
|
30
|
+
callback: (err: Error | null, result: any) => void,
|
|
31
|
+
): void;
|
|
32
|
+
};
|
|
33
|
+
settings: Record<string, any>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface NodeRedContextStore {
|
|
37
|
+
get(
|
|
38
|
+
key: string,
|
|
39
|
+
store: string | undefined,
|
|
40
|
+
callback: (err: Error | null, value: any) => void,
|
|
41
|
+
): void;
|
|
42
|
+
set(
|
|
43
|
+
key: string,
|
|
44
|
+
value: any,
|
|
45
|
+
store: string | undefined,
|
|
46
|
+
callback: (err: Error | null) => void,
|
|
47
|
+
): void;
|
|
48
|
+
keys(
|
|
49
|
+
store: string | undefined,
|
|
50
|
+
callback: (err: Error | null, keys: string[]) => void,
|
|
51
|
+
): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface NodeDefinitionApiResponse {
|
|
55
|
+
type: string;
|
|
56
|
+
align?: "left" | "right";
|
|
57
|
+
category?: "config" | string;
|
|
58
|
+
color?: `#${string}`;
|
|
59
|
+
icon?: string;
|
|
60
|
+
labelStyle?: "node_label" | "node_label_italic" | string;
|
|
61
|
+
paletteLabel?: string;
|
|
62
|
+
inputs?: number;
|
|
63
|
+
outputs?: number;
|
|
64
|
+
inputLabels?: string | string[];
|
|
65
|
+
outputLabels?: string | string[];
|
|
66
|
+
configSchema: TObject | null;
|
|
67
|
+
credentialsSchema: TObject | null;
|
|
68
|
+
inputSchema?: TObject | null;
|
|
69
|
+
outputsSchema?: TObject | null;
|
|
70
|
+
settingsSchema?: TObject | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { RED, NodeDefinitionApiResponse, NodeRedContextStore };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TObject, SchemaOptions } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
interface NodeSchemaOptions extends SchemaOptions {
|
|
4
|
+
"node-type"?: string;
|
|
5
|
+
default?: any;
|
|
6
|
+
format?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getDefaultsFromSchema(
|
|
10
|
+
schema: TObject,
|
|
11
|
+
): Record<string, { type?: string; required: boolean; value: any }> {
|
|
12
|
+
const result: Record<
|
|
13
|
+
string,
|
|
14
|
+
{ type?: string; required: boolean; value: any }
|
|
15
|
+
> = {};
|
|
16
|
+
|
|
17
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
18
|
+
// NOTE: these are excluded from defaults because they must be set by the editor
|
|
19
|
+
if (["x", "y", "z", "g", "wires", "type", "id"].includes(key)) continue;
|
|
20
|
+
|
|
21
|
+
const property = value as NodeSchemaOptions;
|
|
22
|
+
|
|
23
|
+
result[key] = {
|
|
24
|
+
// NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
|
|
25
|
+
required: false,
|
|
26
|
+
value: property.default ?? undefined,
|
|
27
|
+
// NOTE: I'm using a custom json schema keyword to determine the node type
|
|
28
|
+
type: property["node-type"],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getCredentialsFromSchema(
|
|
36
|
+
schema: TObject,
|
|
37
|
+
): Record<string, { type: string; required: boolean; value: any }> {
|
|
38
|
+
const result: Record<
|
|
39
|
+
string,
|
|
40
|
+
{ type: string; required: boolean; value: any }
|
|
41
|
+
> = {};
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
44
|
+
const property = value as NodeSchemaOptions;
|
|
45
|
+
result[key] = {
|
|
46
|
+
// NOTE: required is always false because it is controlled by the JSON Schema and AJV validation instead of using node-red client core
|
|
47
|
+
required: false,
|
|
48
|
+
type: property.format === "password" ? "password" : "text",
|
|
49
|
+
value: property.default ?? undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export { getDefaultsFromSchema, getCredentialsFromSchema };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Validator } from "../validator";
|
|
2
|
+
import type { RED } from "./types";
|
|
3
|
+
|
|
4
|
+
class NodeRedValidator extends Validator {
|
|
5
|
+
constructor(RED: RED) {
|
|
6
|
+
super({
|
|
7
|
+
customKeywords: [
|
|
8
|
+
{ keyword: "skip-validation", schemaType: "boolean", valid: true },
|
|
9
|
+
{
|
|
10
|
+
keyword: "node-type",
|
|
11
|
+
type: "string",
|
|
12
|
+
validate: (schemaValue: string, dataValue: string) => {
|
|
13
|
+
if (!dataValue) return true;
|
|
14
|
+
const node = RED.nodes.getNode(dataValue);
|
|
15
|
+
return node?.type === schemaValue;
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
customFormats: {
|
|
20
|
+
"node-id": /^[a-zA-Z0-9-_]+$/,
|
|
21
|
+
"flow-id": /^[a-f0-9]{16}$/,
|
|
22
|
+
"topic-path": (data: string) => /^[a-zA-Z0-9/_-]+$/.test(data),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export let validator: NodeRedValidator = undefined!;
|
|
29
|
+
|
|
30
|
+
export function initValidator(RED: RED): void {
|
|
31
|
+
validator = new NodeRedValidator(RED);
|
|
32
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Options,
|
|
3
|
+
ErrorObject,
|
|
4
|
+
ErrorsTextOptions,
|
|
5
|
+
AnySchemaObject,
|
|
6
|
+
ValidateFunction,
|
|
7
|
+
KeywordDefinition,
|
|
8
|
+
} from "ajv";
|
|
9
|
+
import Ajv from "ajv";
|
|
10
|
+
import addFormats from "ajv-formats";
|
|
11
|
+
import addErrors from "ajv-errors";
|
|
12
|
+
|
|
13
|
+
interface ValidationResult<T = unknown> {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
data?: T;
|
|
16
|
+
errors?: ErrorObject[];
|
|
17
|
+
errorMessage?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ValidatorOptions extends Options {
|
|
21
|
+
customKeywords?: string[] | KeywordDefinition[];
|
|
22
|
+
customFormats?: Record<string, RegExp | ((data: string) => boolean)>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface DetailedError {
|
|
26
|
+
field: string;
|
|
27
|
+
message: string;
|
|
28
|
+
keyword: string;
|
|
29
|
+
params: Record<string, unknown>;
|
|
30
|
+
schemaPath: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ValidateOption {
|
|
34
|
+
cacheKey?: string;
|
|
35
|
+
throwOnError?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class Validator {
|
|
39
|
+
private readonly ajv: Ajv;
|
|
40
|
+
|
|
41
|
+
public constructor(options?: ValidatorOptions) {
|
|
42
|
+
const { customKeywords, customFormats, ...ajvOptions } = options || {};
|
|
43
|
+
|
|
44
|
+
this.ajv = new Ajv({
|
|
45
|
+
allErrors: true,
|
|
46
|
+
code: {
|
|
47
|
+
source: false,
|
|
48
|
+
},
|
|
49
|
+
coerceTypes: true,
|
|
50
|
+
removeAdditional: false,
|
|
51
|
+
strict: false,
|
|
52
|
+
strictSchema: false,
|
|
53
|
+
useDefaults: true,
|
|
54
|
+
validateFormats: true,
|
|
55
|
+
// NOTE: typebox handles validation via typescript
|
|
56
|
+
// NOTE: if true, types that are not serializable JSON, like Function, would not work
|
|
57
|
+
validateSchema: false,
|
|
58
|
+
verbose: true,
|
|
59
|
+
...ajvOptions,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
addFormats(this.ajv);
|
|
63
|
+
addErrors(this.ajv);
|
|
64
|
+
|
|
65
|
+
this.addCustomKeywords(customKeywords || []);
|
|
66
|
+
this.addCustomFormats(customFormats || {});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add custom keywords to the validator
|
|
71
|
+
*/
|
|
72
|
+
private addCustomKeywords(keywords?: string[] | KeywordDefinition[]): void {
|
|
73
|
+
if (!keywords) return;
|
|
74
|
+
keywords.forEach((keyword) => {
|
|
75
|
+
this.ajv.addKeyword(keyword);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Add custom formats to the validator
|
|
81
|
+
*/
|
|
82
|
+
private addCustomFormats(
|
|
83
|
+
formats?: Record<string, RegExp | ((data: string) => boolean)>,
|
|
84
|
+
): void {
|
|
85
|
+
if (!formats) return;
|
|
86
|
+
|
|
87
|
+
Object.entries(formats).forEach(([name, validator]) => {
|
|
88
|
+
if (validator instanceof RegExp) {
|
|
89
|
+
this.ajv.addFormat(name, validator);
|
|
90
|
+
} else {
|
|
91
|
+
this.ajv.addFormat(name, { validate: validator });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a validator function with caching
|
|
98
|
+
* @param schema - JSON Schema to validate against
|
|
99
|
+
* @param cacheKey - Optional cache key for reusing validators
|
|
100
|
+
*/
|
|
101
|
+
public createValidator(
|
|
102
|
+
schema: AnySchemaObject,
|
|
103
|
+
cacheKey?: string,
|
|
104
|
+
): ValidateFunction {
|
|
105
|
+
if (cacheKey && !schema.$id) {
|
|
106
|
+
schema.$id = cacheKey;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (schema.$id) {
|
|
110
|
+
const cached = this.ajv.getSchema(schema.$id);
|
|
111
|
+
if (cached) return cached;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const validator = this.ajv.compile(schema);
|
|
115
|
+
|
|
116
|
+
return validator;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Validate data against a schema and return a structured result
|
|
121
|
+
*/
|
|
122
|
+
public validate<T = unknown>(
|
|
123
|
+
data: unknown,
|
|
124
|
+
schema: AnySchemaObject,
|
|
125
|
+
options?: ValidateOption,
|
|
126
|
+
): ValidationResult<T> {
|
|
127
|
+
const validator = this.createValidator(schema, options?.cacheKey);
|
|
128
|
+
const valid = validator(data);
|
|
129
|
+
|
|
130
|
+
if (!valid) {
|
|
131
|
+
const errorMessage = this.formatErrors(validator.errors);
|
|
132
|
+
|
|
133
|
+
if (options?.throwOnError) {
|
|
134
|
+
throw new ValidationError(errorMessage, validator.errors || []);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
valid: false,
|
|
139
|
+
errors: validator.errors || undefined,
|
|
140
|
+
errorMessage,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
valid: true,
|
|
146
|
+
data: data as T,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format errors into a human-readable string
|
|
152
|
+
*/
|
|
153
|
+
public formatErrors(
|
|
154
|
+
errors?: ErrorObject[] | null,
|
|
155
|
+
options?: ErrorsTextOptions,
|
|
156
|
+
): string {
|
|
157
|
+
if (!errors || errors.length === 0) {
|
|
158
|
+
return "No errors";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return this.ajv.errorsText(errors, {
|
|
162
|
+
separator: "; ",
|
|
163
|
+
dataVar: "data",
|
|
164
|
+
...options,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get detailed error information
|
|
170
|
+
*/
|
|
171
|
+
public getDetailedErrors(errors?: ErrorObject[] | null): DetailedError[] {
|
|
172
|
+
if (!errors || errors.length === 0) return [];
|
|
173
|
+
|
|
174
|
+
return errors.map((error) => ({
|
|
175
|
+
field: error.instancePath || "/",
|
|
176
|
+
message: error.message || "Validation failed",
|
|
177
|
+
keyword: error.keyword,
|
|
178
|
+
params: error.params,
|
|
179
|
+
schemaPath: error.schemaPath,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Add a schema to the validator for reference
|
|
185
|
+
*/
|
|
186
|
+
public addSchema(schema: AnySchemaObject, key?: string): this {
|
|
187
|
+
this.ajv.addSchema(schema, key);
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Remove a schema from the validator
|
|
193
|
+
*/
|
|
194
|
+
public removeSchema(key: string): this {
|
|
195
|
+
this.ajv.removeSchema(key);
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Custom error class for validation errors
|
|
202
|
+
*/
|
|
203
|
+
class ValidationError extends Error {
|
|
204
|
+
constructor(
|
|
205
|
+
message: string,
|
|
206
|
+
public readonly errors: ErrorObject[],
|
|
207
|
+
) {
|
|
208
|
+
super(message);
|
|
209
|
+
this.name = "ValidationError";
|
|
210
|
+
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const validator = new Validator();
|
|
215
|
+
|
|
216
|
+
export { Validator, ValidationError, validator };
|
|
217
|
+
export type {
|
|
218
|
+
ValidationResult,
|
|
219
|
+
ValidatorOptions,
|
|
220
|
+
ValidateOption,
|
|
221
|
+
DetailedError,
|
|
222
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": false,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"checkJs": false,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"noImplicitAny": false,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"strict": true,
|
|
19
|
+
"stripInternal": true,
|
|
20
|
+
"target": "ES2022"
|
|
21
|
+
},
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function debounce<T extends (...args: any[]) => any>(
|
|
2
|
+
fn: T,
|
|
3
|
+
delay: number,
|
|
4
|
+
): (...args: Parameters<T>) => void {
|
|
5
|
+
let timeout: NodeJS.Timeout | null = null;
|
|
6
|
+
|
|
7
|
+
return (...args: Parameters<T>) => {
|
|
8
|
+
if (timeout) clearTimeout(timeout);
|
|
9
|
+
timeout = setTimeout(() => fn(...args), delay);
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function withTimeout<T>(
|
|
14
|
+
promise: Promise<T>,
|
|
15
|
+
ms: number,
|
|
16
|
+
fallback?: T,
|
|
17
|
+
): Promise<T> {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const timeout = setTimeout(() => {
|
|
20
|
+
if (fallback !== undefined) {
|
|
21
|
+
resolve(fallback);
|
|
22
|
+
} else {
|
|
23
|
+
reject(new Error(`Timeout after ${ms}ms`));
|
|
24
|
+
}
|
|
25
|
+
}, ms);
|
|
26
|
+
|
|
27
|
+
promise
|
|
28
|
+
.then((result) => {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
resolve(result);
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
reject(error);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function retry<T>(
|
|
40
|
+
fn: () => Promise<T>,
|
|
41
|
+
options: { attempts?: number; delay?: number } = {},
|
|
42
|
+
): Promise<T> {
|
|
43
|
+
const { attempts = 3, delay = 1000 } = options;
|
|
44
|
+
|
|
45
|
+
let lastError: Error | undefined;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < attempts; i++) {
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
lastError = error as Error;
|
|
52
|
+
if (i < attempts - 1) {
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw lastError;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { debounce, withTimeout, retry };
|