UNPKG

jinaga

Version:

Data management for web and mobile applications.

304 lines (230 loc) 10.5 kB
# Self-Inverse Implementation History ## Overview This document describes the self-inverse functionality that existed in Jinaga's inverse specification system prior to commit 85cf396, why it was removed, and the implications of that removal. ## What Was Self-Inverse? ### Purpose Self-inverse was a mechanism that allowed specifications to be reactive to their own **given facts** being added to the system, not just to result facts. ### The Problem It Solved In normal inverse specification generation: - Observers listen for **result types** to be added (e.g., Office, Manager, President) - They do NOT listen for **given types** to be added (e.g., Company when it's the given) - If a given fact arrives after subscription, no mechanism triggers re-evaluation **Example Problem Scenario:** ```typescript const spec = model.given(Office).match((office, facts) => office.company.predecessor().selectMany(company => facts.ofType(President) .join(president => president.office.company, company) .select(president => Jinaga.hash(president)) ) ); // Watch for presidents const observer = j.watch(spec, office, callback); // If 'office' is added AFTER the watch is established, // the callback won't trigger because there's no inverse // listener for Office type arrival ``` ### How Self-Inverse Worked #### 1. Detection Logic The system detected when a specification needed self-inverse by checking: - Exactly one given fact - First operation involves predecessor navigation (moving from given to its predecessors) - Specification has multiple matches (indicating `selectMany` pattern) From the test context provided: ```typescript // Detection logic identified patterns like: // - office.company.predecessor().selectMany(...) // - Specifications starting with .predecessor() that then branch out ``` #### 2. Self-Inverse Creation When conditions were met, the system created a special inverse: ```typescript const selfInverse: SpecificationInverse = { inverseSpecification: specification, // Original specification, not inverted operation: "add", // Trigger on addition givenSubset: [givenName], // The given fact's label name parentSubset: [givenName], // Same as given path: "", // Root path (not nested) resultSubset: [] // Empty - this IS the root }; ``` Key characteristics: - `inverseSpecification` was the **original specification**, not truly inverted - When given fact was saved, this inverse would trigger - Would execute the **full original specification** from the newly arrived given - Ensured results were found and callback invoked #### 3. Reactive Behavior When given fact arrived: 1. `ObservableSource.notifyFactSaved(givenFact)` fired 2. Self-inverse listener matched (listening for given type) 3. Triggered `onResult()` with the given fact as starting point 4. Re-executed full specification query 5. Found matching results and invoked callback ## Why It Was Removed ### Commit 85cf396 Removal The self-inverse functionality was removed due to several issues: ### 1. Infinite Loop Problems **The Issue**: In complex specification structures, self-inverse generation could create infinite loops during the inversion process. **Evidence**: Test file `test/specification/infiniteLoopSpec.ts` was created to document and prevent infinite loop issues: ```typescript // Test case showing disconnected specification that caused infinite loop const specification: Specification = { given: [{ label: { name: "p1", type: "User" }, conditions: [] }], matches: [ { unknown: { name: "u1", type: "GameChallenge" }, conditions: [...] }, { unknown: { name: "u2", type: "GameHub" }, conditions: [] }, { unknown: { name: "u3", type: "Player" }, conditions: [] }, // Complex interconnected matches... ], projection: { type: "composite", components: [] } }; // This would cause infinite loop in shakeTree() or inversion logic ``` **Root Cause**: The `shakeTree()` function in `src/specification/inverse.ts` (lines 85-129) could enter infinite loops when: - Matches had circular dependencies - Labels weren't properly connected to the graph - Self-inverse added another layer of complexity to an already complex inversion process ### 2. Complexity and Maintainability **The Problem**: Self-inverse added 47+ lines of complex code to the inverse generation process: - Detection logic was subtle and hard to reason about - Required special casing in multiple places - Interacted with shakeTree in non-obvious ways - Made the inversion process harder to debug ### 3. Disconnected Specification Detection The removal coincided with improvements to disconnected specification detection (`src/specification/UnionFind.ts`). The team likely decided that: - Disconnected specs should be rejected outright - Self-inverse was trying to paper over deeper structural issues - Better to fail fast than create complex workarounds ### 4. Experimental Status Comments in tests suggested self-inverse was experimental: ```typescript // With broader self-inverse coverage, specifications that reference givens get self-inverses ``` This indicates it was: - Being iteratively refined - Not fully baked - Causing maintenance burden relative to benefit ## The Regression ### What Was Lost Removing self-inverse eliminated the ability for subscriptions to: 1. **React to given fact arrivals** - If given added after subscription, no callback 2. **Recover from failed initial reads** - If initial query returns empty, no retry mechanism 3. **Handle async persistence timing** - No way to re-query after data fully synchronized ### When It Manifests The regression appears when: 1. **Given fact not yet persisted** when subscription starts 2. **IndexedDB still indexing** when initial read executes 3. **Complex nested specifications** that are timing-sensitive 4. **Async operations** creating windows between persist and query ### Real-World Impact **LaunchKings Admin Example**: - Added nested specification to show votes within rounds - Initial read failed due to IndexedDB timing - No self-inverse to retry when round arrived - Callback never invoked - **Required workaround**: Two-stage useEffect to ensure persistence before subscribe This pattern is now **required in all applications** using: - Nested specifications - Dynamic given facts - Subscribe/watch with facts that might arrive asynchronously ## Technical Deep Dive ### The Detection Logic (How It Worked) The self-inverse detection looked for specific patterns: ```typescript // Pattern 1: Predecessor navigation at start office.company.predecessor() // Pattern 2: SelectMany after predecessor .selectMany(company => ...) // Pattern 3: Single given fact model.given(Office) // Not model.given(Office, User) ``` When ALL conditions met: - Create self-inverse listening for given type (Office) - When Office saved, trigger full specification execution - Ensure reactive behavior even when given arrives late ### The Infinite Loop Problem (Why It Failed) **Scenario that caused loops**: ```typescript // Specification with circular dependencies const spec = { given: [User], matches: [ { u1: GameChallenge, conditions: [{ path to u2 }] }, { u2: GameHub, conditions: [] }, // No path - causes shake tree to loop { u3: Player, conditions: [] }, { u4: GameSession, conditions: [{ path to u2 }] }, // u2 has no path conditions but u1 and u4 reference it ] }; ``` In `shakeTree()`: 1. Try to move match for given to front 2. Invert path conditions and move them 3. Move matches with no paths down 4. **Loop detected**: Match returns to original position 5. Throw error: "Labels not connected to the rest of the graph" **Self-inverse made this worse by**: - Adding another inversion layer - Creating self-referential specs - Complicating the shakeTree logic ### The Fix-Removal Tradeoff **Removed** (to fix): - Self-inverse complexity - Infinite loop potential - Maintenance burden **Lost** (regression): - Reactive given fact handling - Recovery from failed reads - Natural async fact handling ## Current State Without Self-Inverse ### Limitations Applications must now: 1. **Ensure given facts exist** before subscribing 2. **Use workarounds** like two-stage useEffect patterns 3. **Handle timing manually** instead of relying on reactive system 4. **Accept callback failures** in certain async scenarios ### Affected Patterns **Broken Patterns**: ```typescript // This DOESN'T work reliably: const round = new VotingRound(event, new Date()); const observer = j.subscribe(spec, round, callback); await j.fact(round); // Too late - subscription already started ``` **Required Workaround**: ```typescript // This works but is awkward: const round = await j.fact(new VotingRound(event, new Date())); await new Promise(resolve => setTimeout(resolve, 100)); // Wait for indexing const observer = j.subscribe(spec, round, callback); ``` ## Path Forward ### Restoration Goals A careful restoration of self-inverse should: 1. **Fix the regression** - Make callbacks fire when given arrives late 2. **Avoid infinite loops** - Use safer detection logic 3. **Minimal scope** - Support simple cases, reject complex ones 4. **Clear failure modes** - Fail fast on unsupported patterns ### Safe Subset to Support **Support self-inverse for**: - Single given fact specifications - Any projection structure (including nested) - Where given fact is a concrete type (not complex pattern) **Do NOT support**: - Multiple given facts - Given facts with complex conditions - Patterns that previously caused infinite loops - Disconnected specifications ### Testing Requirements Must verify: - Voting round scenario works without workarounds - No infinite loops in complex cases - Performance acceptable (< 10ms overhead) - Existing tests still pass - Clear error messages for unsupported patterns ## References - **Implementation Plan**: `docs/plans/SELF_INVERSE_RESTORATION_PLAN.md` - **Test Suite**: `test/specification/nestedSubscriptionSpec.ts` - 33 tests documenting race conditions - **Infinite Loop Prevention**: `test/specification/infiniteLoopSpec.ts` - Guards against regression - **Inverse Logic**: `src/specification/inverse.ts` - Core inversion implementation - **Observer Logic**: `src/observer/observer.ts` - Where self-inverse listeners would register