@api3/commons 0.4.1 → 0.5.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.
Files changed (32) hide show
  1. package/dist/blockchain-utilities/derivation.d.ts +78 -0
  2. package/dist/blockchain-utilities/derivation.d.ts.map +1 -0
  3. package/dist/blockchain-utilities/derivation.js +97 -0
  4. package/dist/blockchain-utilities/derivation.js.map +1 -0
  5. package/dist/blockchain-utilities/schema.d.ts +12 -0
  6. package/dist/blockchain-utilities/schema.d.ts.map +1 -0
  7. package/dist/blockchain-utilities/schema.js +13 -0
  8. package/dist/blockchain-utilities/schema.js.map +1 -0
  9. package/dist/processing/processing.d.ts +70 -18
  10. package/dist/processing/processing.d.ts.map +1 -1
  11. package/dist/processing/processing.js +145 -37
  12. package/dist/processing/processing.js.map +1 -1
  13. package/dist/processing/schema.d.ts +25 -2
  14. package/dist/processing/schema.d.ts.map +1 -1
  15. package/dist/processing/schema.js +10 -9
  16. package/dist/processing/schema.js.map +1 -1
  17. package/dist/processing/unsafe-evaluate.d.ts +1 -0
  18. package/dist/processing/unsafe-evaluate.d.ts.map +1 -1
  19. package/dist/processing/unsafe-evaluate.js +31 -1
  20. package/dist/processing/unsafe-evaluate.js.map +1 -1
  21. package/package.json +5 -3
  22. package/src/blockchain-utilities/README.md +5 -0
  23. package/src/blockchain-utilities/derivation.test.ts +147 -0
  24. package/src/blockchain-utilities/derivation.ts +116 -0
  25. package/src/blockchain-utilities/schema.test.ts +23 -0
  26. package/src/blockchain-utilities/schema.ts +14 -0
  27. package/src/processing/README.md +89 -14
  28. package/src/processing/processing.test.ts +429 -111
  29. package/src/processing/processing.ts +196 -47
  30. package/src/processing/schema.ts +21 -8
  31. package/src/processing/unsafe-evaluate.test.ts +220 -1
  32. package/src/processing/unsafe-evaluate.ts +39 -0
@@ -1,20 +1,29 @@
1
1
  import { type Endpoint, RESERVED_PARAMETERS } from '@api3/ois';
2
2
  import { type GoAsyncOptions, go } from '@api3/promise-utils';
3
3
 
4
- import { type ApiCallParameters, validateApiCallParameters } from './schema';
5
- import { unsafeEvaluate, unsafeEvaluateAsync } from './unsafe-evaluate';
4
+ import {
5
+ type EndpointParameters,
6
+ postProcessingV2ResponseSchema,
7
+ endpointParametersSchema,
8
+ preProcessingV2ResponseSchema,
9
+ type PreProcessingV2Response,
10
+ type PostProcessingV2Response,
11
+ type ProcessingSpecificationV2,
12
+ type ProcessingSpecifications,
13
+ } from './schema';
14
+ import { unsafeEvaluate, unsafeEvaluateAsync, unsafeEvaluateV2 } from './unsafe-evaluate';
6
15
 
7
16
  export const DEFAULT_PROCESSING_TIMEOUT_MS = 10_000;
8
17
 
9
18
  const reservedParameters = RESERVED_PARAMETERS as string[]; // To avoid strict TS checks.
10
19
 
11
20
  /**
12
- * Removes reserved parameters from the parameters object.
13
- * @param parameters The API call parameters from which reserved parameters will be removed.
14
- * @returns The parameters object without reserved parameters.
21
+ * Removes reserved parameters from the endpoint parameters.
22
+ * @param parameters The endpoint parameters from which reserved parameters will be removed.
23
+ * @returns The endpoint parameters without reserved parameters.
15
24
  */
16
- export const removeReservedParameters = (parameters: ApiCallParameters): ApiCallParameters => {
17
- const result: ApiCallParameters = {};
25
+ export const removeReservedParameters = (parameters: EndpointParameters): EndpointParameters => {
26
+ const result: EndpointParameters = {};
18
27
 
19
28
  for (const key in parameters) {
20
29
  if (!reservedParameters.includes(key)) {
@@ -26,15 +35,15 @@ export const removeReservedParameters = (parameters: ApiCallParameters): ApiCall
26
35
  };
27
36
 
28
37
  /**
29
- * Re-inserts reserved parameters from the initial parameters object into the modified parameters object.
30
- * @param initialParameters The initial API call parameters that might contain reserved parameters.
31
- * @param modifiedParameters The modified API call parameters to which reserved parameters will be added.
32
- * @returns The modified parameters object with re-inserted reserved parameters.
38
+ * Re-inserts reserved parameters from the initial endpoint parameters into the modified endpoint parameters.
39
+ * @param initialParameters The initial endpoint parameters that might contain reserved parameters.
40
+ * @param modifiedParameters The modified endpoint parameters to which reserved parameters will be added.
41
+ * @returns The modified endpoint parameters with re-inserted reserved parameters.
33
42
  */
34
43
  export const addReservedParameters = (
35
- initialParameters: ApiCallParameters,
36
- modifiedParameters: ApiCallParameters
37
- ): ApiCallParameters => {
44
+ initialParameters: EndpointParameters,
45
+ modifiedParameters: EndpointParameters
46
+ ): EndpointParameters => {
38
47
  for (const key in initialParameters) {
39
48
  if (reservedParameters.includes(key)) {
40
49
  modifiedParameters[key] = initialParameters[key];
@@ -45,38 +54,38 @@ export const addReservedParameters = (
45
54
  };
46
55
 
47
56
  /**
48
- * Pre-processes API call parameters based on the provided endpoint's processing specifications.
57
+ * Pre-processes endpoint parameters based on the provided endpoint's processing specifications.
49
58
  *
50
- * @param endpoint The endpoint containing processing specifications.
51
- * @param apiCallParameters The parameters to be pre-processed.
59
+ * @param preProcessingSpecifications The v1 pre-processing specifications.
60
+ * @param endpointParameters The parameters to be pre-processed.
52
61
  * @param processingOptions Options to control the async processing behavior like retries and timeouts.
53
62
  *
54
63
  * @returns A promise that resolves to the pre-processed parameters.
55
64
  */
56
- export const preProcessApiCallParameters = async (
57
- endpoint: Endpoint,
58
- apiCallParameters: ApiCallParameters,
65
+ export const preProcessEndpointParametersV1 = async (
66
+ preProcessingSpecifications: ProcessingSpecifications | undefined,
67
+ endpointParameters: EndpointParameters,
59
68
  processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
60
- ): Promise<ApiCallParameters> => {
61
- const { preProcessingSpecifications } = endpoint;
69
+ ): Promise<EndpointParameters> => {
62
70
  if (!preProcessingSpecifications || preProcessingSpecifications.length === 0) {
63
- return apiCallParameters;
71
+ return endpointParameters;
64
72
  }
65
73
 
66
- // We only wrap the code through "go" utils because of the timeout and retry logic.
74
+ // We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
75
+ // just re-throws.
67
76
  const goProcessedParameters = await go(async () => {
68
- let currentValue: unknown = removeReservedParameters(apiCallParameters);
77
+ let currentValue: unknown = removeReservedParameters(endpointParameters);
69
78
 
70
79
  for (const processing of preProcessingSpecifications) {
71
80
  // Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
72
81
  // snippet independently because processing snippets can modify the parameters.
73
- const endpointParameters = removeReservedParameters(apiCallParameters);
82
+ const nonReservedEndpointParameters = removeReservedParameters(endpointParameters);
74
83
 
75
84
  switch (processing.environment) {
76
85
  case 'Node': {
77
86
  currentValue = await unsafeEvaluate(
78
87
  processing.value,
79
- { input: currentValue, endpointParameters },
88
+ { input: currentValue, endpointParameters: nonReservedEndpointParameters },
80
89
  processing.timeoutMs
81
90
  );
82
91
  break;
@@ -84,7 +93,7 @@ export const preProcessApiCallParameters = async (
84
93
  case 'Node async': {
85
94
  currentValue = await unsafeEvaluateAsync(
86
95
  processing.value,
87
- { input: currentValue, endpointParameters },
96
+ { input: currentValue, endpointParameters: nonReservedEndpointParameters },
88
97
  processing.timeoutMs
89
98
  );
90
99
  break;
@@ -97,46 +106,48 @@ export const preProcessApiCallParameters = async (
97
106
  if (!goProcessedParameters.success) throw goProcessedParameters.error;
98
107
 
99
108
  // Let this throw if the processed parameters are invalid.
100
- const parsedParameters = validateApiCallParameters(goProcessedParameters.data);
109
+ const parsedParameters = endpointParametersSchema.parse(goProcessedParameters.data);
101
110
 
102
- // Having removed reserved parameters for pre-processing, we need to re-insert them for the API call.
103
- return addReservedParameters(apiCallParameters, parsedParameters);
111
+ // Having removed reserved parameters for pre-processing, we need to re-insert them after pre-processing.
112
+ return addReservedParameters(endpointParameters, parsedParameters);
104
113
  };
105
114
 
106
115
  /**
107
- * Post-processes the API call response based on the provided endpoint's processing specifications.
116
+ * Post-processes the response based on the provided endpoint's processing specifications. The response is usually the
117
+ * API call response, but this logic depends on how the processing is used by the target service. For example, Airnode
118
+ * allows skipping API calls in which case the response is the result of pre-processing.
108
119
  *
109
- * @param apiCallResponse The raw response obtained from the API call.
110
- * @param endpoint The endpoint containing processing specifications.
111
- * @param apiCallParameters The parameters used in the API call.
120
+ * @param response The response to be post-processed.
121
+ * @param postProcessingSpecifications The v1 post-processing specifications.
122
+ * @param endpointParameters The endpoint parameters.
112
123
  * @param processingOptions Options to control the async processing behavior like retries and timeouts.
113
124
  *
114
- * @returns A promise that resolves to the post-processed API call response.
125
+ * @returns A promise that resolves to the post-processed response.
115
126
  */
116
- export const postProcessApiCallResponse = async (
117
- apiCallResponse: unknown,
118
- endpoint: Endpoint,
119
- apiCallParameters: ApiCallParameters,
127
+ export const postProcessResponseV1 = async (
128
+ response: unknown,
129
+ postProcessingSpecifications: ProcessingSpecifications | undefined,
130
+ endpointParameters: EndpointParameters,
120
131
  processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
121
132
  ) => {
122
- const { postProcessingSpecifications } = endpoint;
123
133
  if (!postProcessingSpecifications || postProcessingSpecifications?.length === 0) {
124
- return apiCallResponse;
134
+ return response;
125
135
  }
126
136
 
127
- // We only wrap the code through "go" utils because of the timeout and retry logic.
137
+ // We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
138
+ // just re-throws.
128
139
  const goResult = await go(async () => {
129
- let currentValue: unknown = apiCallResponse;
140
+ let currentValue: unknown = response;
130
141
 
131
142
  for (const processing of postProcessingSpecifications) {
132
143
  // Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
133
144
  // snippet independently because processing snippets can modify the parameters.
134
- const endpointParameters = removeReservedParameters(apiCallParameters);
145
+ const nonReservedEndpointParameters = removeReservedParameters(endpointParameters);
135
146
  switch (processing.environment) {
136
147
  case 'Node': {
137
148
  currentValue = await unsafeEvaluate(
138
149
  processing.value,
139
- { input: currentValue, endpointParameters },
150
+ { input: currentValue, endpointParameters: nonReservedEndpointParameters },
140
151
  processing.timeoutMs
141
152
  );
142
153
  break;
@@ -144,7 +155,7 @@ export const postProcessApiCallResponse = async (
144
155
  case 'Node async': {
145
156
  currentValue = await unsafeEvaluateAsync(
146
157
  processing.value,
147
- { input: currentValue, endpointParameters },
158
+ { input: currentValue, endpointParameters: nonReservedEndpointParameters },
148
159
  processing.timeoutMs
149
160
  );
150
161
  break;
@@ -158,3 +169,141 @@ export const postProcessApiCallResponse = async (
158
169
 
159
170
  return goResult.data;
160
171
  };
172
+
173
+ /**
174
+ * Pre-processes endpoint parameters based on the provided endpoint's processing specifications.
175
+ *
176
+ * @param preProcessingSpecificationV2 The v2 pre-processing specification.
177
+ * @param endpointParameters The parameters to be pre-processed.
178
+ * @param processingOptions Options to control the async processing behavior like retries and timeouts.
179
+ *
180
+ * @returns A promise that resolves to the pre-processed parameters.
181
+ */
182
+ export const preProcessEndpointParametersV2 = async (
183
+ preProcessingSpecificationV2: ProcessingSpecificationV2 | undefined,
184
+ endpointParameters: EndpointParameters,
185
+ processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
186
+ ): Promise<PreProcessingV2Response> => {
187
+ if (!preProcessingSpecificationV2) return { endpointParameters };
188
+
189
+ // We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
190
+ // just re-throws.
191
+ const goProcessedParameters = await go(async () => {
192
+ const { environment, timeoutMs, value } = preProcessingSpecificationV2;
193
+
194
+ switch (environment) {
195
+ case 'Node': {
196
+ return unsafeEvaluateV2(value, { endpointParameters: removeReservedParameters(endpointParameters) }, timeoutMs);
197
+ }
198
+ }
199
+ }, processingOptions);
200
+ if (!goProcessedParameters.success) throw goProcessedParameters.error;
201
+
202
+ // Let this throw if the processed parameters are invalid.
203
+ const preProcessingResponse = preProcessingV2ResponseSchema.parse(goProcessedParameters.data);
204
+
205
+ // Having removed reserved parameters for pre-processing, we need to re-insert them after pre-processing.
206
+ return { endpointParameters: addReservedParameters(endpointParameters, preProcessingResponse.endpointParameters) };
207
+ };
208
+
209
+ /**
210
+ * Post-processes the response based on the provided endpoint's processing specifications. The response is usually the
211
+ * API call response, but this logic depends on how the processing is used by the target service. For example, Airnode
212
+ * allows skipping API calls in which case the response is the result of pre-processing.
213
+ *
214
+ * @param response The response to be post-processed.
215
+ * @param postProcessingSpecificationV2 The v2 post-processing specification.
216
+ * @param endpointParameters The endpoint parameters.
217
+ * @param processingOptions Options to control the async processing behavior like retries and timeouts.
218
+ *
219
+ * @returns A promise that resolves to the post-processed response.
220
+ */
221
+ export const postProcessResponseV2 = async (
222
+ response: unknown,
223
+ postProcessingSpecificationV2: ProcessingSpecificationV2 | undefined,
224
+ endpointParameters: EndpointParameters,
225
+ processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
226
+ ): Promise<PostProcessingV2Response> => {
227
+ if (!postProcessingSpecificationV2) return { response };
228
+
229
+ // We only wrap the code through "go" utils because of the timeout and retry logic. In case of error, the function
230
+ // just re-throws.
231
+ const goResult = await go(async () => {
232
+ const { environment, timeoutMs, value } = postProcessingSpecificationV2;
233
+ // Provide endpoint parameters without reserved parameters immutably between steps. Recompute them for each
234
+ // snippet independently because processing snippets can modify the parameters.
235
+ const nonReservedEndpointParameters = removeReservedParameters(endpointParameters);
236
+
237
+ switch (environment) {
238
+ case 'Node': {
239
+ return unsafeEvaluateV2(value, { response, endpointParameters: nonReservedEndpointParameters }, timeoutMs);
240
+ }
241
+ }
242
+ }, processingOptions);
243
+ if (!goResult.success) throw goResult.error;
244
+
245
+ return postProcessingV2ResponseSchema.parse(goResult.data);
246
+ };
247
+
248
+ /**
249
+ * Pre-processes endpoint parameters based on the provided endpoint's processing specifications. Internally it
250
+ * determines what processing implementation should be used.
251
+ *
252
+ * @param endpoint The endpoint containing processing specifications.
253
+ * @param endpointParameters The parameters to be pre-processed.
254
+ * @param processingOptions Options to control the async processing behavior like retries and timeouts.
255
+ *
256
+ * @returns A promise that resolves to the pre-processed parameters.
257
+ */
258
+ export const preProcessEndpointParameters = async (
259
+ endpoint: Endpoint,
260
+ endpointParameters: EndpointParameters,
261
+ processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
262
+ ): Promise<PreProcessingV2Response> => {
263
+ const { preProcessingSpecificationV2, preProcessingSpecifications } = endpoint;
264
+ if (preProcessingSpecificationV2) {
265
+ return preProcessEndpointParametersV2(preProcessingSpecificationV2, endpointParameters);
266
+ }
267
+
268
+ const preProcessV1Response = await preProcessEndpointParametersV1(
269
+ preProcessingSpecifications,
270
+ endpointParameters,
271
+ processingOptions
272
+ );
273
+ return { endpointParameters: preProcessV1Response };
274
+ };
275
+
276
+ /**
277
+ * Post-processes the response based on the provided endpoint's processing specifications. The response is usually the
278
+ * API call response, but this logic depends on how the processing is used by the target service. For example, Airnode
279
+ * allows skipping API calls in which case the response is the result of pre-processing.
280
+ *
281
+ * This function determines what processing version should be used and provides a common interface. This is useful for
282
+ * services that want to use processing and don't care which processing version is used.
283
+ *
284
+ * @param response The response to be post-processed.
285
+ * @param endpoint The endpoint containing processing specifications.
286
+ * @param endpointParameters The endpoint parameters.
287
+ * @param processingOptions Options to control the async processing behavior like retries and timeouts.
288
+ *
289
+ * @returns A promise that resolves to the post-processed response.
290
+ */
291
+ export const postProcessResponse = async (
292
+ response: unknown,
293
+ endpoint: Endpoint,
294
+ endpointParameters: EndpointParameters,
295
+ processingOptions: GoAsyncOptions = { retries: 0, totalTimeoutMs: DEFAULT_PROCESSING_TIMEOUT_MS }
296
+ ): Promise<PostProcessingV2Response> => {
297
+ const { postProcessingSpecificationV2, postProcessingSpecifications } = endpoint;
298
+ if (postProcessingSpecificationV2) {
299
+ return postProcessResponseV2(response, postProcessingSpecificationV2, endpointParameters);
300
+ }
301
+
302
+ const postProcessV1Response = await postProcessResponseV1(
303
+ response,
304
+ postProcessingSpecifications,
305
+ endpointParameters,
306
+ processingOptions
307
+ );
308
+ return { response: postProcessV1Response };
309
+ };
@@ -1,10 +1,23 @@
1
- export const validateApiCallParameters = (apiCallParameters: unknown): ApiCallParameters => {
2
- // eslint-disable-next-line lodash/prefer-lodash-typecheck
3
- if (typeof apiCallParameters !== 'object' || apiCallParameters === null) {
4
- throw new TypeError('Invalid API call parameters');
5
- }
1
+ import type { processingSpecificationSchemaV2, ProcessingSpecification } from '@api3/ois';
2
+ import { z } from 'zod';
6
3
 
7
- return apiCallParameters as ApiCallParameters;
8
- };
4
+ export type ProcessingSpecificationV2 = z.infer<typeof processingSpecificationSchemaV2>;
9
5
 
10
- export type ApiCallParameters = Record<string, any>;
6
+ export type ProcessingSpecifications = ProcessingSpecification[];
7
+
8
+ export const endpointParametersSchema = z.record(z.any());
9
+
10
+ export type EndpointParameters = z.infer<typeof endpointParametersSchema>;
11
+
12
+ export const preProcessingV2ResponseSchema = z.object({
13
+ endpointParameters: endpointParametersSchema,
14
+ });
15
+
16
+ export type PreProcessingV2Response = z.infer<typeof preProcessingV2ResponseSchema>;
17
+
18
+ export const postProcessingV2ResponseSchema = z.object({
19
+ response: z.any(),
20
+ timestamp: z.number().nonnegative().int().optional(),
21
+ });
22
+
23
+ export type PostProcessingV2Response = z.infer<typeof postProcessingV2ResponseSchema>;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable jest/prefer-strict-equal */ // Because the errors are thrown from the "vm" module (different context), they are not strictly equal.
2
- import { unsafeEvaluate, unsafeEvaluateAsync } from './unsafe-evaluate';
2
+ import { unsafeEvaluate, unsafeEvaluateAsync, unsafeEvaluateV2 } from './unsafe-evaluate';
3
3
 
4
4
  describe('unsafe evaluate - sync', () => {
5
5
  it('executes harmless code', () => {
@@ -101,3 +101,222 @@ describe('unsafe evaluate - async', () => {
101
101
  );
102
102
  });
103
103
  });
104
+
105
+ describe(unsafeEvaluateV2.name, () => {
106
+ it('has access to node modules and vm context', async () => {
107
+ const res = await unsafeEvaluateV2(
108
+ `
109
+ async () => {
110
+ return Object.keys(this);
111
+ }
112
+ `,
113
+ {},
114
+ 100
115
+ );
116
+
117
+ expect(res).toEqual([
118
+ 'assert',
119
+ 'async_hooks',
120
+ 'buffer',
121
+ 'child_process',
122
+ 'cluster',
123
+ 'console',
124
+ 'constants',
125
+ 'crypto',
126
+ 'dgram',
127
+ 'dns',
128
+ 'events',
129
+ 'fs',
130
+ 'http',
131
+ 'http2',
132
+ 'https',
133
+ 'inspector',
134
+ 'module',
135
+ 'net',
136
+ 'os',
137
+ 'path',
138
+ 'perf_hooks',
139
+ 'process',
140
+ 'readline',
141
+ 'repl',
142
+ 'stream',
143
+ 'string_decoder',
144
+ 'timers',
145
+ 'tls',
146
+ 'trace_events',
147
+ 'tty',
148
+ 'url',
149
+ 'util',
150
+ 'v8',
151
+ 'vm',
152
+ 'worker_threads',
153
+ 'zlib',
154
+ 'setTimeout',
155
+ 'setInterval',
156
+ 'clearTimeout',
157
+ 'clearInterval',
158
+ 'payload',
159
+ ]);
160
+ });
161
+
162
+ it('can access vm context values as global variables', async () => {
163
+ const res = await unsafeEvaluateV2(
164
+ `
165
+ async (payload) => {
166
+ return [this.payload, payload];
167
+ }
168
+ `,
169
+ 123,
170
+ 100
171
+ );
172
+
173
+ expect(res).toEqual([123, 123]);
174
+ });
175
+
176
+ it('works with sync function as well', async () => {
177
+ const res = await unsafeEvaluateV2(
178
+ `
179
+ (payload) => {
180
+ return payload + 500;
181
+ }
182
+ `,
183
+ 123,
184
+ 100
185
+ );
186
+
187
+ expect(res).toBe(623);
188
+ });
189
+
190
+ it('can use setTimeout and setInterval', async () => {
191
+ const res = await unsafeEvaluateV2(
192
+ `
193
+ async (payload) => {
194
+ const intervalId = setInterval(() => payload++, 50);
195
+ setTimeout(() => payload++, 50);
196
+ await new Promise((resolve) => {
197
+ clearInterval(intervalId);
198
+ setTimeout(resolve, 120);
199
+ });
200
+ return payload;
201
+ }
202
+ `,
203
+ 0,
204
+ 250
205
+ );
206
+
207
+ expect(res).toBe(3);
208
+ });
209
+
210
+ it('applies timeout', async () => {
211
+ await expect(async () =>
212
+ unsafeEvaluateV2(
213
+ `
214
+ async () => {
215
+ await new Promise((res) => setTimeout(res, 100));
216
+ }
217
+ `,
218
+ 0,
219
+ 50
220
+ )
221
+ ).rejects.toEqual(new Error('Full timeout exceeded'));
222
+ });
223
+
224
+ it('rejects on sync error', async () => {
225
+ await expect(async () =>
226
+ unsafeEvaluateV2(
227
+ `
228
+ () => {
229
+ return nonDefinedPayload + 500;
230
+ }
231
+ `,
232
+ {},
233
+ 100
234
+ )
235
+ ).rejects.toEqual(new ReferenceError('nonDefinedPayload is not defined'));
236
+
237
+ await expect(async () =>
238
+ unsafeEvaluateV2(
239
+ `
240
+ () => {
241
+ throw new Error('some error');
242
+ }
243
+ `,
244
+ {},
245
+ 100
246
+ )
247
+ ).rejects.toEqual(new Error('some error'));
248
+
249
+ await expect(async () =>
250
+ unsafeEvaluateV2(
251
+ `
252
+ () => {
253
+ throw 'non-error-value';
254
+ }
255
+ `,
256
+ {},
257
+ 100
258
+ )
259
+ ).rejects.toBe('non-error-value');
260
+ });
261
+
262
+ it('allows using closures for modular code', async () => {
263
+ const res = await unsafeEvaluateV2(
264
+ `
265
+ async (payload) => {
266
+ const isDivisible = (n, k) => n % k === 0;
267
+
268
+ const isPrime = (n) => {
269
+ for (let i = 2; i < n; i++) {
270
+ if (isDivisible(n, i)) return false;
271
+ }
272
+ return true;
273
+ }
274
+
275
+ const ans = []
276
+ for (let i = 2; i < payload; i++) {
277
+ if (isPrime(i)) ans.push(i);
278
+ }
279
+ return ans;
280
+ }
281
+ `,
282
+ 50,
283
+ 1000
284
+ );
285
+
286
+ expect(res).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]);
287
+ });
288
+
289
+ it('can use any async function syntax', async () => {
290
+ const anonymousArrowFnResult = await unsafeEvaluateV2(
291
+ `
292
+ async (payload) => {
293
+ return await Promise.resolve(payload + 500);
294
+ }
295
+ `,
296
+ 123,
297
+ 100
298
+ );
299
+ const regularFnResult = await unsafeEvaluateV2(
300
+ `
301
+ async function(payload) {
302
+ return await Promise.resolve(payload + 500);
303
+ }
304
+ `,
305
+ 123,
306
+ 100
307
+ );
308
+ const regularFnReturningPromiseResult = await unsafeEvaluateV2(
309
+ `
310
+ function(payload) {
311
+ return Promise.resolve(payload + 500);
312
+ }
313
+ `,
314
+ 123,
315
+ 100
316
+ );
317
+
318
+ expect(anonymousArrowFnResult).toBe(623);
319
+ expect(regularFnResult).toBe(623);
320
+ expect(regularFnReturningPromiseResult).toBe(623);
321
+ });
322
+ });
@@ -36,6 +36,8 @@ import vm from 'node:vm';
36
36
  import worker_threads from 'node:worker_threads';
37
37
  import zlib from 'node:zlib';
38
38
 
39
+ import { type GoWrappedError, go } from '@api3/promise-utils';
40
+
39
41
  import { createTimers } from './vm-timers';
40
42
 
41
43
  const builtInNodeModules = {
@@ -113,6 +115,43 @@ export const unsafeEvaluate = (code: string, globalVariables: Record<string, unk
113
115
  return vmContext.deferredOutput;
114
116
  };
115
117
 
118
+ export const unsafeEvaluateV2 = async (code: string, payload: unknown, timeout: number) => {
119
+ const timers = createTimers();
120
+
121
+ const goEvaluate = await go<Promise<any>, GoWrappedError>(
122
+ // eslint-disable-next-line @typescript-eslint/require-await
123
+ async () =>
124
+ vm.runInNewContext(
125
+ `
126
+ (async () => {
127
+ return await (${code})(payload)
128
+ })();
129
+ `,
130
+ {
131
+ ...builtInNodeModules,
132
+ setTimeout: timers.customSetTimeout,
133
+ setInterval: timers.customSetInterval,
134
+ clearTimeout: timers.customClearTimeout,
135
+ clearInterval: timers.customClearInterval,
136
+ payload,
137
+ },
138
+ { displayErrors: true, timeout }
139
+ ),
140
+ // Make sure the timeout is applied. When the processing snippet uses setTimeout or setInterval, the timeout option
141
+ // from VM is broken. See: https://github.com/nodejs/node/issues/3020.
142
+ { totalTimeoutMs: timeout }
143
+ );
144
+
145
+ // We need to manually clear all timers and reject the processing manually.
146
+ timers.clearAll();
147
+
148
+ if (goEvaluate.success) {
149
+ return goEvaluate.data;
150
+ } else {
151
+ throw (goEvaluate.error.reason as Error) ?? goEvaluate.error;
152
+ }
153
+ };
154
+
116
155
  /**
117
156
  * Asynchronously evaluates the provided code in a new VM context with the specified global variables.
118
157
  *