To enable NDiff for a DAO, add .setNdiff(true) in your services.jrl:
return new foam.dao.EasyDAO.Builder(x)
.setNdiff(true)
.setJournalName("mydata")
.setOf(com.example.MyModel.getOwnClassInfo())
.build();
Once enabled, access Admin > NDiffs to view changed objects.
NDiff is a debugging and tracking system for changes to CSpecs (Component Specifications) in FOAM3. It captures the initial state of objects loaded at startup from the repository journal (.0 file) and any frozen journal generations, and compares them to their current runtime state, enabling developers to:
| File | Purpose |
|---|---|
foam3/src/foam/core/ndiff/NDiff.js |
Data model storing comparison data |
foam3/src/foam/core/ndiff/NDiffDAO.js |
DAO decorator that intercepts puts and logs changes |
foam3/src/foam/core/ndiff/NDiffRuntimeDAO.js |
Processes NDiff records at runtime with filtering |
foam3/src/foam/core/ndiff/NDiffJournal.js |
Journal wrapper integrating NDiff into replay pipeline |
foam3/src/foam/core/ndiff/services.jrl |
Service configuration for ndiffDAO |
foam3/src/foam/core/ndiff/menus.jrl |
Admin UI menu configuration |
foam3/src/foam/u2/view/ComparisonView.js |
Side-by-side comparison UI |
+-------------------------------------------------------------------------+
| NDiff System Architecture |
+-------------------------------------------------------------------------+
| |
| +---------------------------------------------------------------------+ |
| | JDAO (Journal DAO) | |
| | +-----------------------+ +----------------------------+ | |
| | | Repo Journal (.0) | | Runtime Journal | | |
| | | | | | | |
| | | NDiffJournal | | NDiffJournal | | |
| | | (runtimeOrigin=false)| | (runtimeOrigin=true) | | |
| | +-----------+-----------+ +-------------+----------------+ | |
| | | | | |
| +--------------|-------------------------------|------------------------+ |
| | | |
| v v |
| +----------------------------------------------------------------------+ |
| | NDiffDAO | |
| | Wraps delegate DAO during replay | |
| | Records initialFObject for each put() | |
| | Only records from repo journal (runtimeOrigin=false) | |
| +----------------------------------+-----------------------------------+ |
| | |
| v |
| +----------------------------------------------------------------------+ |
| | ndiffDAO Service | |
| | +------------------------------------------------------------------+ | |
| | | NDiffRuntimeDAO | | |
| | | - Filters select() to show only changed/deleted records | | |
| | | - Populates runtimeFObject on-the-fly | | |
| | | - Handles applyOriginal to restore initial state | | |
| | +------------------------------------------------------------------+ | |
| +----------------------------------------------------------------------+ |
| |
+---------------------------------------------------------------------------+
| Property | Type | Description |
|---|---|---|
cSpecName |
String | Name of the CSpec (service) this object belongs to |
objectId |
String | ID of the tracked object |
initialFObject |
FObjectProperty | Object as loaded from repo journal (.0 file) |
runtimeFObject |
FObjectProperty | Current runtime state (transient, populated during select) |
deletedAtRuntime |
Boolean | True if object was deleted after initial load |
applyOriginal |
Boolean | Flag to trigger restoration of initial state |
NDiff uses a composite ID: (cSpecName, objectId). Each NDiff record tracks ONE object from ONE CSpec.
runtimeOrigin=false: Data comes from the repo journal (.0 file) or a frozen generation (.N, .N.gz, .N.snap.gz) - recorded to NDiffruntimeOrigin=true: Data comes from runtime journal - not recorded (just passes through)+-------------------------------------------------------------------------+
| System Startup |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Step 1: Load Repo Journal (flows.0) |
| |
| Journal Entry --> NDiffJournal(runtimeOrigin=false) |
| | |
| v |
| NDiffDAO.put_() |
| | |
| +--> Store object in target DAO |
| | |
| +--> Create NDiff record with initialFObject |
| | |
| v |
| ndiffDAO.put() |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Step 2: Load Runtime Journal (flows) |
| |
| Journal Entry --> NDiffJournal(runtimeOrigin=true) |
| | |
| v |
| NDiffDAO.put_() |
| | |
| +--> Store object in target DAO (NO NDiff) |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Step 3: User Queries NDiff (Admin Panel) |
| |
| ndiffDAO.select() --> NDiffRuntimeDAO.select_() |
| | |
| +--> Apply deltaPredicate filter: |
| | - Object deleted? (runtimeFObject=null)|
| | - Object changed? (!equals check) |
| | |
| +--> Populate runtimeFObject from target DAO|
| | |
| v |
| Return filtered results |
+-------------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------------+
| Step 4: User Actions |
| |
| "Compare Changes" --> Opens ComparisonView modal |
| | |
| +--> Shows side-by-side: |
| Before: initialFObject |
| After: runtimeFObject (or "Deleted") |
| |
| "Apply Original" --> Sets applyOriginal=true |
| | |
| +--> NDiffRuntimeDAO.put_() detects flag |
| | |
| +--> Restores initialFObject |
| to target DAO |
+-------------------------------------------------------------------------+
The NDiffRuntimeDAO.select_() only returns NDiff records where the object was either deleted at runtime OR the current runtime state differs from the initial state loaded from the repo journal.
In your services.jrl, add .setNdiff(true) to the EasyDAO.Builder chain for any DAO you want to track.
+------------------------------------------------------------------------+
| NDiffs |
+--------------+--------------+----------------+------------+-------------+
| CSpec | Object ID | Deleted | Actions | |
+--------------+--------------+----------------+------------+-------------+
| flowDAO | flow-001 | false | [Apply] | [Compare] |
| flowDAO | flow-002 | true | [Apply] | [Compare] |
| userDAO | user-123 | false | [Apply] | [Compare] |
+--------------+--------------+----------------+------------+-------------+
+-------------------------------------------------------------------------+
| Comparison: flow-001 |
+-------------------------------------------------------------------------+
| |
| +-------------------------------------------------------------------+ |
| | Changed fields: [name] [status] [updatedAt] | |
| +-------------------------------------------------------------------+ |
| |
| +---------------------------+ +---------------------------+ |
| | Before | | After | |
| +---------------------------+ +---------------------------+ |
| | id: flow-001 | | id: flow-001 | |
| | name: "Original Name" | | name: "Updated Name" | |
| | status: DRAFT | | status: ACTIVE | |
| | createdAt: 2024-01-01 | | createdAt: 2024-01-01 | |
| | updatedAt: 2024-01-01 | | updatedAt: 2024-01-15 | |
| +---------------------------+ +---------------------------+ |
| |
+-------------------------------------------------------------------------+
| State | Display | Color |
|---|---|---|
| Deleted | “Object was deleted at runtime” | Red background |
| Changed | “Changed fields: [field1] [field2]…” | Yellow chips |
| Unchanged | “No changes detected” | Green background |
NDiff is opt-in: Objects are NOT tracked by default. You must enable it per-DAO with .setNdiff(true).
Only pre-runtime journal entries are recorded: Runtime journal entries pass through without creating NDiff records. Entries replayed ahead of the runtime journal are recorded, which includes frozen generations as well as the .0 repo journal, so after a roll or a compaction the initial state also contains earlier runtime data.
runtimeFObject is transient: The runtimeFObject property is NOT persisted - it’s fetched on-the-fly during select() operations. This ensures you always see the current state.
Apply Original creates recursive calls: When you apply the original, it puts to the target DAO which is decorated by NDiffDAO. The system handles this correctly by re-fetching the NDiff after the put.
Filter limitations in UI: Filtering and sorting in views may not work correctly because the predicate is applied before the sink can make changes. This is a known limitation.
Composite ID required: To find an NDiff record programmatically, you need both cSpecName and objectId using NDiffId.
Works with JDAO only: NDiff integrates at the JDAO level (journal-backed DAOs). It won’t track changes to in-memory-only DAOs or direct database DAOs.