@fincity/kirun-js 3.2.2 → 3.4.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.
@@ -747,11 +747,9 @@ export class ExpressionEvaluator {
747
747
  if (key.length > 2 && valuesMap.has(key))
748
748
  workingStack.push(new ExpressionTokenValue(str, this.getValue(str, valuesMap)));
749
749
  else {
750
- let v: any;
751
- try {
752
- v = LiteralTokenValueExtractor.INSTANCE.getValue(str);
753
- } catch (err) {
754
- // Check if this is a literal (number, string, boolean, null) with property access
750
+ let v: any = LiteralTokenValueExtractor.INSTANCE.tryGetValue(str);
751
+ if (LiteralTokenValueExtractor.isFailed(v)) {
752
+ // Not a valid literal — check if it's a literal with property access
755
753
  // e.g., "2.val" should evaluate to undefined (accessing .val on number 2)
756
754
  v = this.evaluateLiteralPropertyAccess(str);
757
755
  }
@@ -774,11 +772,9 @@ export class ExpressionEvaluator {
774
772
  const basePart = str.substring(0, dotIdx);
775
773
  const propPart = str.substring(dotIdx + 1);
776
774
 
777
- // Try to parse the base as a literal
778
- let baseValue: any;
779
- try {
780
- baseValue = LiteralTokenValueExtractor.INSTANCE.getValue(basePart);
781
- } catch (err) {
775
+ // Try to parse the base as a literal without throwing
776
+ const baseValue: any = LiteralTokenValueExtractor.INSTANCE.tryGetValue(basePart);
777
+ if (LiteralTokenValueExtractor.isFailed(baseValue)) {
782
778
  // Not a valid literal, return the original string
783
779
  return str;
784
780
  }
@@ -1,14 +1,25 @@
1
- import { StringFormatter } from '../../../util/string/StringFormatter';
2
-
3
1
  export class ExpressionEvaluationException extends Error {
4
2
  cause?: Error;
3
+ private readonly _expression: string;
4
+ private readonly _msg: string;
5
5
 
6
6
  constructor(expression: string, message: string, err?: Error) {
7
- super(StringFormatter.format('$ : $', expression, message));
8
-
7
+ // Temporarily disable stack trace capture — this exception is used as control flow
8
+ // in hot paths, and V8 stack trace capture is very expensive.
9
+ const prevLimit = (Error as any).stackTraceLimit;
10
+ (Error as any).stackTraceLimit = 0;
11
+ super();
12
+ (Error as any).stackTraceLimit = prevLimit;
13
+ this._expression = expression;
14
+ this._msg = message;
9
15
  this.cause = err;
10
16
  }
11
17
 
18
+ // Lazy message formatting — only computed when actually read (e.g. logging)
19
+ override get message(): string {
20
+ return this._expression + ' : ' + this._msg;
21
+ }
22
+
12
23
  public getCause(): Error | undefined {
13
24
  return this.cause;
14
25
  }
@@ -13,6 +13,11 @@ const KEYWORDS: Map<string, any> = new Map([
13
13
  export class LiteralTokenValueExtractor extends TokenValueExtractor {
14
14
  public static readonly INSTANCE: LiteralTokenValueExtractor = new LiteralTokenValueExtractor();
15
15
 
16
+ public static readonly FAILED: unique symbol = Symbol('LITERAL_PARSE_FAILED');
17
+
18
+ // Cache for number parsing: stores the parsed number, or FAILED symbol for non-numeric tokens
19
+ private static readonly numberCache: Map<string, number | symbol> = new Map();
20
+
16
21
  protected getValueInternal(token: string): any {
17
22
  if (StringUtil.isNullOrBlank(token)) return undefined;
18
23
 
@@ -27,20 +32,52 @@ export class LiteralTokenValueExtractor extends TokenValueExtractor {
27
32
  return this.processNumbers(token);
28
33
  }
29
34
 
30
- private processNumbers(token: string): any {
31
- try {
32
- let v = Number(token);
35
+ /**
36
+ * Non-throwing version of getValueInternal. Returns FAILED symbol instead of throwing.
37
+ * This is the hot-path method — avoids ExpressionEvaluationException creation entirely.
38
+ */
39
+ private tryGetValueInternal(token: string): any {
40
+ if (StringUtil.isNullOrBlank(token)) return undefined;
41
+
42
+ token = token.trim();
43
+
44
+ if (KEYWORDS.has(token)) return KEYWORDS.get(token);
45
+
46
+ if (token.startsWith('"')) {
47
+ if (!token.endsWith('"')) return LiteralTokenValueExtractor.FAILED;
48
+ return token.substring(1, token.length - 1);
49
+ }
33
50
 
34
- if (isNaN(v)) throw new Error('Parse number error');
51
+ return this.tryProcessNumbers(token);
52
+ }
35
53
 
36
- return v;
37
- } catch (err: any) {
54
+ private processNumbers(token: string): any {
55
+ const result = this.tryProcessNumbers(token);
56
+ if (result === LiteralTokenValueExtractor.FAILED) {
38
57
  throw new ExpressionEvaluationException(
39
58
  token,
40
- StringFormatter.format('Unable to parse the literal or expression $', token),
41
- err,
59
+ 'Unable to parse the literal or expression ' + token,
42
60
  );
43
61
  }
62
+ return result;
63
+ }
64
+
65
+ /**
66
+ * Non-throwing number parser. Returns FAILED symbol for non-numeric tokens.
67
+ * Results are cached so each unique token is parsed at most once.
68
+ */
69
+ private tryProcessNumbers(token: string): number | symbol {
70
+ const cached = LiteralTokenValueExtractor.numberCache.get(token);
71
+ if (cached !== undefined) return cached;
72
+
73
+ const v = Number(token);
74
+ if (Number.isNaN(v) || token.trim() === '') {
75
+ LiteralTokenValueExtractor.numberCache.set(token, LiteralTokenValueExtractor.FAILED);
76
+ return LiteralTokenValueExtractor.FAILED;
77
+ }
78
+
79
+ LiteralTokenValueExtractor.numberCache.set(token, v);
80
+ return v;
44
81
  }
45
82
 
46
83
  private processString(token: string): any {
@@ -61,6 +98,23 @@ export class LiteralTokenValueExtractor extends TokenValueExtractor {
61
98
  return undefined;
62
99
  }
63
100
 
101
+ /**
102
+ * Try to get a literal value without throwing or creating any exception objects.
103
+ * Returns FAILED symbol if not a valid literal.
104
+ */
105
+ public tryGetValue(token: string): any {
106
+ const prefix = this.getPrefix();
107
+ if (prefix && !token.startsWith(prefix)) return LiteralTokenValueExtractor.FAILED;
108
+ return this.tryGetValueInternal(token);
109
+ }
110
+
111
+ /**
112
+ * Check if a result from tryGetValue indicates failure.
113
+ */
114
+ public static isFailed(value: any): boolean {
115
+ return value === LiteralTokenValueExtractor.FAILED;
116
+ }
117
+
64
118
  public getValueFromExtractors(token: string, maps: Map<string, TokenValueExtractor>): any {
65
119
  if (maps.has(token + '.')) return maps.get(token + '.')?.getStore();
66
120
  return this.getValue(token);
@@ -7,6 +7,7 @@ import { ExpressionEvaluationException } from '../exception/ExpressionEvaluation
7
7
  export abstract class TokenValueExtractor {
8
8
  public static readonly REGEX_SQUARE_BRACKETS: RegExp = /[\[\]]/;
9
9
  public static readonly REGEX_DOT: RegExp = /(?<!\.)\.(?!\.)/;
10
+ private static readonly REGEX_INTEGER: RegExp = /^-?\d+$/;
10
11
 
11
12
  // Cache for parsed paths to avoid repeated regex splits
12
13
  private static pathCache: Map<string, string[]> = new Map();
@@ -177,7 +178,7 @@ export abstract class TokenValueExtractor {
177
178
 
178
179
  // Only use fast path for pure integer strings (no range operators like '..')
179
180
  // Note: parseInt('2..4', 10) incorrectly returns 2, so we need to validate first
180
- if (/^-?\d+$/.test(segment)) {
181
+ if (TokenValueExtractor.REGEX_INTEGER.test(segment)) {
181
182
  const idx = Number.parseInt(segment, 10);
182
183
  const actualIdx = idx < 0 ? element.length + idx : idx;
183
184
  return actualIdx >= 0 && actualIdx < element.length ? element[actualIdx] : undefined;
@@ -188,7 +189,7 @@ export abstract class TokenValueExtractor {
188
189
  if (typeof element === 'string') {
189
190
  if (segment === 'length') return element.length;
190
191
  // Only use fast path for pure integer strings
191
- if (/^-?\d+$/.test(segment)) {
192
+ if (TokenValueExtractor.REGEX_INTEGER.test(segment)) {
192
193
  const idx = Number.parseInt(segment, 10);
193
194
  const actualIdx = idx < 0 ? element.length + idx : idx;
194
195
  return actualIdx >= 0 && actualIdx < element.length ? element[actualIdx] : undefined;