Modern JavaScript in Encompass SSF

Modern JavaScript in Encompass SSF (Server-Side Framework)

Overview

Encompass uses the Server-Side Framework (SSF) to execute JavaScript plugins. Understanding SSF architecture, its capabilities, and limitations is critical for building reliable plugins.


Part 1: SSF Architecture and JavaScript Execution

What is SSF?

SSF (Server-Side Framework) is Encompass’s plugin execution engine that:

  • Executes JavaScript plugins on the server
  • Provides access to loan objects, form data, and user context
  • Manages event subscriptions and lifecycle
  • Enforces security and sandboxing

JavaScript Execution Context

/* $PLG:TARGET=Multi */ - this is a magical line - will discuss some other time
"use strict";

// SSF provides these global objects:
// - elli.script          (main API)
// - loan                 (loan object)
// - auth                 (authentication)
// - user                 (user object)
// - app                  (application object)

// Your plugin runs in:
// - Server-side context (not browser)
// - Sandboxed environment
// - Synchronous AND asynchronous operations
// - Event-driven architecture

Event-Based Architecture

SSF plugins are event-driven:

// Subscribe to events
elli.script.subscribe("application", "login", userLoginHandler);
elli.script.subscribe("loan", "precommit", loanPrecommitHandler);
elli.script.subscribe("loan", "postcommit", loanPostcommitHandler);
elli.script.subscribe("loan", "close", loanCloseHandler);

// Handlers receive events and can:
// - Access loan data
// - Modify loan data
// - Return true/false to allow/block operations
// - Call other async functions

Part 2: The JavaScript File System Challenge

Challenge 1: Limited API Surface

The Problem:

// What you WANT to do (doesn't exist):
const borrowerInfo = await loan.getBorrowerInfo();
const appliedRules = await loan.getAppliedRules();
const pricingData = await loan.getPricingDetails();

// What you CAN do (limited):
const fieldValue = await loan.getField("4000");
const fields = await loan.getFields();
const fieldIds = await loan.getFieldIds();

Why It Matters:

  • Only field-level access available
  • No direct access to calculated values
  • Must reconstruct business logic from fields
  • Performance impact (multiple field calls)

Challenge 2: Field Access Inconsistency

The Problem:

// These might work... or might not, depending on context:
const value1 = await loan.getField("4000");           // Method 1
const value2 = await loan.getFieldValue("4000");      // Method 2
const value3 = loan["4000"];                          // Method 3
const value4 = loan.fields["4000"];                   // Method 4
const value5 = await loan.getFieldForBorrowerPair("4000", 0); // Method 5

// Which one works depends on:
// - Encompass version
// - Context (precommit, postcommit, etc.)
// - Borrower pair configuration
// - Data availability

Best Practice:

async function getFieldValue(loan, fieldId, pairIndex = 0) {
  try {
    // Try Method 1
    if (loan.getField && typeof loan.getField === "function") {
      const value = await loan.getField(fieldId);
      if (value !== undefined) return value;
    }

    // Try Method 2
    if (loan.getFieldValue && typeof loan.getFieldValue === "function") {
      const value = await loan.getFieldValue(fieldId);
      if (value !== undefined) return value;
    }

    // Try Method 3
    if (loan.getFieldForBorrowerPair && typeof loan.getFieldForBorrowerPair === "function") {
      const value = await loan.getFieldForBorrowerPair(fieldId, pairIndex);
      if (value !== undefined) return value;
    }

    // Fallback
    return loan[fieldId] || null;
  } catch (err) {
    console.warn(`Error getting field [${fieldId}]:`, err);
    return null;
  }
}

Challenge 3: String-Based Field Values

The Problem:

// Field 4143 "Information Provided" returns string values:
const infoValue = await loan.getField("4143");
console.log(infoValue); // "FACETOFACE" (string)
console.log(typeof infoValue); // "string"

// Checkbox fields return STRING, not boolean:
const ethnicity = await loan.getField("4210");
console.log(ethnicity); // "true" or "false" (string, not boolean!)
console.log(typeof ethnicity); // "string"

// This causes bugs:
if (ethnicity) { // Always truthy, even if "false"
  // This ALWAYS executes, even when checkbox is unchecked!
}

// Correct approach:
if (ethnicity === "true" || ethnicity === "1" || ethnicity === "Y") {
  // Now correctly checks the actual value
}

Why This Matters:

  • Easy to write buggy validation logic
  • “false” string is truthy in JavaScript
  • Must explicitly compare to strings
  • Type coercion causes unexpected behavior

Challenge 4: Async/Await Complexity

The Problem:

// SSF operations are async, but timing is tricky:

async function setFieldAndTrigger() {
  // This LOOKS like it waits:
  await loan.setFields({ "field1": "value1" });
  
  // But Field Trigger Rules might execute:
  // - Immediately (fire and forget)
  // - Asynchronously (later)
  // - In parallel (same time)
  // - After your function returns (async queue)
  
  // So THIS might be unreliable:
  await loan.setFields({ "CX.HMDA.STATUS": "Incomplete" });
  return false; // Returns immediately
  // Meanwhile, Field Trigger Rule is STILL executing in background
}

// The Field Trigger Rule popup might appear AFTER your function returns
// Encompass regains focus BEFORE popup shows
// Popup appears BEHIND Encompass window

Challenge 5: Field State Uncertainty

The Problem:

// You don't always know the current field state:

async function triggerValidation() {
  // What is the current state of "CX.HMDA.CHANGED"?
  // - Could be "Y" (from previous validation)
  // - Could be "" (if reset worked)
  // - Could be undefined (never set)
  // - Could be something else (user edited it)
  
  // Setting to "Y" might or might not be a change:
  await loan.setFields({ "CX.HMDA.CHANGED": "Y" });
  // Field Trigger Rule sees: ?? → "Y"
  // If previous state was "Y", no change detected!
}

// Solution: Explicit reset first
async function triggerValidationSafely() {
  // GUARANTEE empty state
  await loan.setFields({ "CX.HMDA.CHANGED": "" });
  await delay(100);
  
  // NOW set to trigger
  // GUARANTEED change: "" → "Y"
  await loan.setFields({ "CX.HMDA.CHANGED": "Y" });
}

Challenge 6: Popup/Alert Suppression

The Problem:

// These might be suppressed:
window.alert("Message");           // ❌ Suppressed
alert("Message");                  // ❌ Suppressed

// These might work (but not guaranteed):
elli.script.alert("Title", "Message");      // Maybe ✓
elli.script.showDialog({...});              // Maybe ✓
elli.script.notify({...});                  // Maybe ✓

// Best approach: Use Field Trigger Rule instead
// Set a field → Field Trigger Rule fires MsgBox
// This is MORE reliable than direct popups

Challenge 7: Borrower Pair Handling

The Problem:

// Getting borrower pair information is unclear:

// Method 1: Direct array access
const borrowerPairs = loan.getBorrowerPairs(); // Maybe returns [0, 1, 2]?
// Maybe returns [0]? Maybe returns undefined?

// Method 2: Field access per pair
const borrower1Name = await loan.getFieldForBorrowerPair("4000", 0);
const borrower2Name = await loan.getFieldForBorrowerPair("4000", 1);

// You must handle:
// - Non-existent pairs
// - Missing pair indices
// - Undefined getBorrowerPairs()
// - Different pair numbering (0-based? 1-based?)

Best Practice:

async function iterateBorrowerPairs(loan) {
  try {
    const borrowerPairs = loan.getBorrowerPairs ? loan.getBorrowerPairs() : [0];
    
    for (let i = 0; i < (borrowerPairs?.length || 1); i++) {
      const pairIndex = borrowerPairs?.[i] ?? i;
      
      // Check if borrower exists in this pair
      const borrowerName = await getFieldValue(loan, "4000", pairIndex);
      if (borrowerName && borrowerName.toString().trim() !== "") {
        // Process this borrower
        console.log(`Borrower ${i}:`, borrowerName);
      }
    }
  } catch (err) {
    console.error("Error iterating pairs:", err);
  }
}

Challenge 8: No Direct C# Interoperability

The Problem:

// You CANNOT directly:
// - Call C# methods
// - Access C# properties
// - Read from C# forms (BorrowerSummary.cs)
// - Access C# UI elements
// - Query C# calculated values

// Example: BorrowerSummary.cs has a label "lbGovMonitoringStatus"
// In JavaScript, you CANNOT do:
const labelValue = BorrowerSummary.lbGovMonitoringStatus.Text; // ❌ Doesn't work

// Solutions:
// 1. Store C# value in hidden field → Read field from JS
// 2. Mirror C# logic in JavaScript
// 3. Re-implement validation in JavaScript

Part 3: Comprehensive Challenges and Solutions

Challenge 9: Event Timing and Race Conditions

The Problem:

async function loanPrecommit(app) {
  // Precommit happens BEFORE loan saves
  // But multiple things might trigger simultaneously:
  
  // Your plugin is running
  // Field Trigger Rules are running
  // Other plugins might be running
  // User actions might be happening
  
  // Race condition scenario:
  await loan.setFields({ "CX.HMDA.CHANGED": "Y" });
  // Field Trigger Rule STARTS executing
  return false; // You return false immediately
  // Field Trigger Rule popup HASN'T APPEARED YET
  // Encompass regains focus BEFORE popup visible
  // Popup appears BEHIND Encompass ❌
}

// Solution: Explicit wait
async function loanPrecommit(app) {
  await loan.setFields({ "CX.HMDA.CHANGED": "" }); // Reset first
  await loan.setFields({ "CX.HMDA.CHANGED": "Y" }); // Then trigger
  
  // Wait for Field Trigger Rule to execute
  console.log("Waiting for Field Trigger Rule...");
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  // NOW Field Trigger Rule has fired and popup is visible
  return false;
}

Challenge 10: No Debugging Tools

The Problem:

// You cannot:
// - Use browser DevTools (running server-side)
// - Inspect objects with debugger
// - Set breakpoints
// - Monitor network calls
// - Inspect HTML/DOM

// Your ONLY debugging tool is:
console.log("Debug message");

// This appears in Encompass trace logs
// Must submit to trace, copy to text file, analyze manually
// Inefficient for complex debugging

Best Practice:

// Comprehensive logging strategy:
function logDebug(context, message, data = {}) {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] [${context}] ${message}`, data);
}

logDebug("HMDA Plugin", "Validation started", { 
  borrowerName: "John Doe",
  fields: ["4210", "4211"]
});

logDebug("HMDA Plugin", "Field value retrieved", {
  fieldId: "4210",
  value: "true"
});

logDebug("HMDA Plugin", "Validation completed", {
  result: "PASSED",
  duration: "234ms"
});

Challenge 11: Version and Environment Variations

The Problem:

// Different Encompass versions have different APIs:

// Version A (2021):
const value = await loan.getField("4000");

// Version B (2022):
const value = loan.getFieldValue("4000");

// Version C (2023):
const value = loan.getFieldForBorrowerPair("4000", 0);

// Your code must work across versions
// You need compatibility layers
// Same code might fail in different environments

Solution:

// Create a compatibility wrapper
async function getFieldValue(loan, fieldId, pairIndex = 0) {
  // Try multiple methods in order of likelihood
  
  // Modern approach (newest versions)
  if (typeof loan.getField === "function") {
    try {
      const value = await loan.getField(fieldId);
      if (value !== undefined) return value;
    } catch (err) {
      // Silent fall-through
    }
  }
  
  // Alternative approach
  if (typeof loan.getFieldValue === "function") {
    try {
      const value = await loan.getFieldValue(fieldId);
      if (value !== undefined) return value;
    } catch (err) {
      // Silent fall-through
    }
  }
  
  // Borrower-pair specific
  if (typeof loan.getFieldForBorrowerPair === "function") {
    try {
      const value = await loan.getFieldForBorrowerPair(fieldId, pairIndex);
      if (value !== undefined) return value;
    } catch (err) {
      // Silent fall-through
    }
  }
  
  // Direct property access (fallback)
  return loan[fieldId] || null;
}

Part 4: Best Practices for SSF JavaScript

1. Always Validate Field Access

// ❌ Don't do this:
const value = await loan.getField("4000");
if (value) { // Might crash if method doesn't exist
  process(value);
}

// ✅ Do this:
async function safeGetField(loan, fieldId) {
  if (!loan || !fieldId) return null;
  
  if (loan.getField && typeof loan.getField === "function") {
    try {
      return await loan.getField(fieldId);
    } catch (err) {
      console.warn(`Error accessing field ${fieldId}:`, err);
      return null;
    }
  }
  
  return null;
}

2. Explicit State Management

// ❌ Don't assume state:
await loan.setFields({ "trigger_field": "Y" });

// ✅ Always reset first:
await loan.setFields({ "trigger_field": "" }); // Reset
await new Promise(r => setTimeout(r, 100));    // Small delay
await loan.setFields({ "trigger_field": "Y" }); // Trigger
await new Promise(r => setTimeout(r, 1000));   // Wait
await loan.setFields({ "trigger_field": "" }); // Reset again

3. Comprehensive Error Handling

try {
  const value = await loan.getField("4000");
  
  if (value === undefined) {
    console.warn("Field returned undefined");
  }
  
  if (value === null) {
    console.warn("Field returned null");
  }
  
  if (value === "") {
    console.warn("Field is empty string");
  }
  
  // Only process known good state
  if (value && value.toString().trim() !== "") {
    processValue(value);
  }
  
} catch (err) {
  if (err instanceof Error) {
    console.error("Error:", err.message);
  } else {
    console.error("Unknown error:", String(err));
  }
}

4. Type Coercion Awareness

// Remember: String field values!

// ❌ Buggy:
const isSelected = fieldValue; // "false" is truthy!

// ✅ Correct:
const isSelected = fieldValue === "true" || fieldValue === "1" || fieldValue === "Y";
const isUnselected = fieldValue === "false" || fieldValue === "0" || fieldValue === "N" || fieldValue === "";

// Handle both string and boolean returns
const checkValue = (value) => {
  if (typeof value === "boolean") return value;
  if (value === "true" || value === "1" || value === "Y") return true;
  if (value === "false" || value === "0" || value === "N" || value === "") return false;
  return !!value;
};

5. Structured Logging

async function validateHMDA(loan) {
  console.log("\n" + "=".repeat(80));
  console.log("HMDA Validation Started");
  console.log("=".repeat(80));
  
  try {
    const borrowerName = await getFieldValue(loan, "4000");
    console.log(`  Borrower: ${borrowerName}`);
    
    // Validation logic...
    const passed = await runValidation(loan);
    
    if (passed) {
      console.log("  ✓ Validation PASSED");
    } else {
      console.error("  ✗ Validation FAILED");
      // Trigger notification
      await setField(loan, "CX.HMDA.CHANGED", "");
      await delay(100);
      await setField(loan, "CX.HMDA.CHANGED", "Y");
    }
    
  } catch (err) {
    console.error("  ERROR:", err.message);
  }
  
  console.log("=".repeat(80) + "\n");
}

Part 5: SSF Plugin Architecture Pattern

Recommended Structure

/* $PLG:TARGET=Multi */
"use strict";

// ============================================
// CONFIGURATION
// ============================================
const CONFIG = {
  FIELD_BORROWER_NAME: "4000",
  FIELD_INFO_PROVIDED: "4143",
  FIELD_TRIGGER: "CX.HMDA.CHANGED"
};

// ============================================
// GLOBAL STATE
// ============================================
let auth = {};
let user = {};
let loan = {};

// ============================================
// HELPER FUNCTIONS
// ============================================
async function getFieldValue(loan, fieldId) {
  // Implementation with fallbacks
}

async function setFieldValue(loan, fieldId, value) {
  // Implementation with error handling
}

// ============================================
// EVENT HANDLERS
// ============================================
async function userLogin(applicationObj) {
  console.log("User login event");
  auth = await elli.script.getObject("auth");
  user = await auth.getUser();
}

async function loanPrecommit(appObj) {
  console.log("Loan precommit event");
  loan = await elli.script.getObject("loan");
  
  try {
    const result = await validateLoan(loan);
    return result;
  } catch (err) {
    console.error("Precommit error:", err);
    return true; // Allow save on error
  }
}

async function loanClosed(appObj) {
  console.log("Loan closed event");
  loan = {};
  auth = {};
  user = {};
}

// ============================================
// MAIN LOGIC
// ============================================
async function validateLoan(loan) {
  // Actual validation logic
  return true;
}

// ============================================
// INITIALIZATION
// ============================================
if (elli && elli.script) {
  elli.script.subscribe("application", "login", userLogin);
  elli.script.subscribe("loan", "precommit", loanPrecommit);
  elli.script.subscribe("loan", "close", loanClosed);
  console.log("Plugin initialized");
}

// ============================================
// EXPORTS (for testing)
// ============================================
if (typeof module !== "undefined" && module.exports) {
  module.exports = {
    validateLoan,
    getFieldValue,
    setFieldValue
  };
}

Conclusion

Key Challenges Summary

Challenge Impact Solution
Limited API Can’t access all data Reconstruct from fields
Field Access Inconsistency Unpredictable behavior Try multiple methods
String Field Values Type coercion bugs Explicit string comparison
Async/Await Timing Race conditions Explicit waits and delays
Field State Uncertainty Triggers don’t fire Reset before triggering
Popup Suppression User can’t see alerts Use Field Trigger Rules
Borrower Pair Handling Multi-borrower complexity Iterative with fallbacks
No C# Interop Can’t call C# code Store in fields, mirror logic
Race Conditions Flaky behavior Wait for operations to complete
No Debugging Hard to diagnose issues Comprehensive logging
Version Variations Code breaks in other versions Compatibility wrappers

Core Principles

Assume nothing – Always verify state
Be explicit – Reset before trigger, wait after
Handle all paths – Try multiple methods
Log extensively – Trace logs are your debugger
Test thoroughly – Edge cases are common
Plan for failure – Use try-catch everywhere


References

  • Encompass SSF Documentation
  • Server-Side Framework API Reference
  • Best Practices for Plugin Development
  • Field Trigger Rule Configuration Guide

You may also like...

Popular Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.