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
