@@ -127,22 +127,29 @@ public void setEvaluateExitCallback(ExitCallback cb) {
127127 * @returns {*} Evaluated input data
128128 */
129129 Object evaluate (Symbol expr , Object input , Frame environment ) {
130- return _evaluate (expr , input , environment );
130+ // Thread safety:
131+ // Make sure each evaluate is executed on an instance per thread
132+ Jsonata _this = getPerThreadInstance ();
133+ // Save and restore the evaluation context so that nested
134+ // evaluations (e.g. $eval()) see the correct context.
135+ Object _input = _this .input ;
136+ Frame _environment = _this .environment ;
137+ try {
138+ return _this ._evaluate (expr , input , environment );
139+ } finally {
140+ _this .input = _input ;
141+ _this .environment = _environment ;
142+ }
131143 }
132144
133145 Object _evaluate (Symbol expr , Object input , Frame environment ) {
134146 Object result = null ;
135147
136- // Save and restore the evaluation context so that nested
137- // evaluations (e.g. $eval()) see the correct context.
138- // All mutable per-evaluation state lives on EvalContext, not on this instance.
139- EvalContext ctx = evalContext .get ();
140- Object savedInput = ctx .input ;
141- Frame savedEnvironment = ctx .environment ;
142- ctx .input = input ;
143- ctx .environment = environment ;
148+ // Store the current input + environment
149+ // This is required by Functions.functionEval for current $eval() input context
150+ this .input = input ;
151+ this .environment = environment ;
144152
145- try {
146153 if (parser .dbg ) System .out .println ("eval expr=" +expr +" type=" +expr .type );//+" input="+input);
147154
148155 var entryCallback = environment .lookup ("__evaluate_entry" );
@@ -239,10 +246,6 @@ Object _evaluate(Symbol expr, Object input, Frame environment) {
239246 }
240247
241248 return result ;
242- } finally {
243- ctx .input = savedInput ;
244- ctx .environment = savedEnvironment ;
245- }
246249 }
247250
248251 /**
@@ -1556,34 +1559,29 @@ boolean isFunctionLike(Object o) {
15561559 return Utils .isFunction (o ) || Functions .isLambda (o ) || (o instanceof Pattern );
15571560 }
15581561
1562+ final static ThreadLocal <Jsonata > current = new ThreadLocal <>();
1563+
15591564 /**
1560- * Mutable evaluation context, stored on a static ThreadLocal.
1561- * Holds all per-evaluation state that was previously stored as mutable fields
1562- * on the Jsonata instance. This allows the Jsonata instance itself to remain
1563- * immutable after construction, making it freely shareable across threads
1564- * with no cloning needed.
1565+ * Returns a per thread instance of this parsed expression.
1566+ *
1567+ * @return
15651568 */
1566- static final class EvalContext {
1567- Object input ;
1568- Frame environment ;
1569- long timestamp ;
1570- final Jsonata instance ;
1571-
1572- EvalContext (Jsonata instance ) {
1573- this .instance = instance ;
1569+ Jsonata getPerThreadInstance () {
1570+ Jsonata threadInst = current .get ();
1571+ // Fast path
1572+ if (threadInst !=null )
1573+ return threadInst ;
1574+
1575+ synchronized (this ) {
1576+ threadInst = current .get ();
1577+ if (threadInst ==null ) {
1578+ threadInst = new Jsonata (this );
1579+ current .set (threadInst );
1580+ }
1581+ return threadInst ;
15741582 }
15751583 }
15761584
1577- /**
1578- * Thread-local evaluation context. Set at the entry point of evaluate()
1579- * and restored after evaluation completes. Read by Functions.java for
1580- * $eval(), $now(), $millis(), and funcApply().
1581- *
1582- * This is the ONLY ThreadLocal needed. No per-instance ThreadLocals,
1583- * no cloning, no mutable state on the Jsonata instance during evaluation.
1584- */
1585- static final ThreadLocal <EvalContext > evalContext = new ThreadLocal <>();
1586-
15871585 /**
15881586 * Evaluate Object against input data
15891587 * @param {Object} expr - JSONata expression
@@ -1594,6 +1592,8 @@ static final class EvalContext {
15941592 /* async */ Object evaluateFunction (Symbol expr , Object input , Frame environment , Object applytoContext ) {
15951593 Object result = null ;
15961594
1595+ // this.current is set by getPerThreadInstance() at this point
1596+
15971597 // create the procedure
15981598 // can"t assume that expr.procedure is a lambda type directly
15991599 // could be an expression that evaluates to a Object (e.g. variable reference, parens expr etc.
@@ -2484,6 +2484,8 @@ Exception populateMessage(Exception err) {
24842484 List <Exception > errors ;
24852485 Frame environment ;
24862486 Symbol ast ;
2487+ long timestamp ;
2488+ Object input ;
24872489
24882490 static {
24892491 staticFrame = new Frame (null );
@@ -2516,6 +2518,8 @@ public static Jsonata jsonata(String expression) {
25162518 }
25172519 environment = createFrame (staticFrame );
25182520
2521+ timestamp = System .currentTimeMillis (); // will be overridden on each call to evalute()
2522+
25192523 // Note: now and millis are implemented in Functions
25202524 // environment.bind("now", defineFunction(function(picture, timezone) {
25212525 // return datetime.fromMillis(timestamp.getTime(), picture, timezone);
@@ -2531,8 +2535,21 @@ public static Jsonata jsonata(String expression) {
25312535 // jsonata.RegexEngine = RegExp;
25322536 // }
25332537
2538+ // Set instance for this thread
2539+ current .set (this );
25342540 }
25352541
2542+ /**
2543+ * Creates a clone of the given Jsonata instance.
2544+ * Package-private copy constructor used to create per thread instances.
2545+ *
2546+ * @param other
2547+ */
2548+ Jsonata (Jsonata other ) {
2549+ this .ast = other .ast ;
2550+ this .environment = other .environment ;
2551+ this .timestamp = other .timestamp ;
2552+ }
25362553
25372554 /**
25382555 * Flag: validate input objects to comply with JSON types
@@ -2574,10 +2591,13 @@ public Object evaluate(Object input, Frame bindings) { // FIXME:, callback) {
25742591 } else {
25752592 exec_env = environment ;
25762593 }
2594+ // put the input document into the environment as the root object
25772595 exec_env .bind ("$" , input );
25782596
2579- // Timestamp for $now() and $millis() — captured once per evaluation
2580- long ts = System .currentTimeMillis ();
2597+ // capture the timestamp and put it in the execution environment
2598+ // the $now() and $millis() functions will return this value - whenever it is called
2599+ timestamp = System .currentTimeMillis ();
2600+ //exec_env.timestamp = timestamp;
25812601
25822602 // if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
25832603 if ((input instanceof List ) && !Utils .isSequence (input )) {
@@ -2588,24 +2608,18 @@ public Object evaluate(Object input, Frame bindings) { // FIXME:, callback) {
25882608 if (validateInput )
25892609 Functions .validateInput (input );
25902610
2591- EvalContext prev = evalContext .get ();
2592- EvalContext ctx = new EvalContext (this );
2593- ctx .input = input ;
2594- ctx .environment = exec_env ;
2595- ctx .timestamp = ts ;
2596- evalContext .set (ctx );
2597-
25982611 Object it ;
25992612 try {
26002613 it = /* await */ evaluate (ast , input , exec_env );
2614+ // if (typeof callback === "function") {
2615+ // callback(null, it);
2616+ // }
26012617 it = Utils .convertNulls (it );
26022618 return it ;
26032619 } catch (Exception err ) {
26042620 // insert error message into structure
26052621 populateMessage (err ); // possible side-effects on `err`
26062622 throw err ;
2607- } finally {
2608- evalContext .set (prev );
26092623 }
26102624 }
26112625
0 commit comments