mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-01-12 07:12:54 +01:00
884 lines
21 KiB
Markdown
884 lines
21 KiB
Markdown
# AGENT-004: Optimistic Update Pattern
|
|
|
|
## Overview
|
|
|
|
Create a pattern and helper nodes for implementing optimistic UI updates - updating the UI immediately before the server confirms the change, then rolling back if the server rejects it. This creates a more responsive user experience for network operations.
|
|
|
|
**Phase:** 3.5 (Real-Time Agentic UI)
|
|
**Priority:** MEDIUM
|
|
**Effort:** 2-3 days
|
|
**Risk:** Low
|
|
|
|
---
|
|
|
|
## Problem Statement
|
|
|
|
### Current Pattern: Slow & Blocking
|
|
|
|
```
|
|
User clicks "Accept Connection"
|
|
↓
|
|
Show loading spinner
|
|
↓
|
|
Wait for server... (300-1000ms)
|
|
↓
|
|
Update UI to show "Connected"
|
|
↓
|
|
Hide spinner
|
|
```
|
|
|
|
**Problem:** User waits, UI feels sluggish.
|
|
|
|
### Desired Pattern: Fast & Optimistic
|
|
|
|
```
|
|
User clicks "Accept Connection"
|
|
↓
|
|
Immediately show "Connected" (optimistic)
|
|
↓
|
|
Send request to server (background)
|
|
↓
|
|
IF success: Do nothing (already updated!)
|
|
↓
|
|
IF failure: Roll back to "Pending", show error
|
|
```
|
|
|
|
**Benefit:** UI feels instant, even with slow network.
|
|
|
|
### Real-World Use Cases (Erleah)
|
|
|
|
1. **Accept Connection Request** - Show "Accepted" immediately
|
|
2. **Add to Timeline** - Item appears instantly
|
|
3. **Send Chat Message** - Message shows while sending
|
|
4. **Toggle Bookmark** - Star fills immediately
|
|
5. **Drag-Drop Reorder** - Items reorder before server confirms
|
|
|
|
---
|
|
|
|
## Goals
|
|
|
|
1. ✅ Apply optimistic update to variable/store
|
|
2. ✅ Commit if backend succeeds
|
|
3. ✅ Rollback if backend fails
|
|
4. ✅ Show pending state (optional)
|
|
5. ✅ Queue multiple optimistic updates
|
|
6. ✅ Handle race conditions (out-of-order responses)
|
|
7. ✅ Integrate with Global Store (AGENT-003)
|
|
|
|
---
|
|
|
|
## Technical Design
|
|
|
|
### Node Specification
|
|
|
|
```javascript
|
|
{
|
|
name: 'net.noodl.OptimisticUpdate',
|
|
displayNodeName: 'Optimistic Update',
|
|
category: 'Data',
|
|
color: 'orange',
|
|
docs: 'https://docs.noodl.net/nodes/data/optimistic-update'
|
|
}
|
|
```
|
|
|
|
### Port Schema
|
|
|
|
#### Inputs
|
|
|
|
| Port Name | Type | Group | Description |
|
|
|-----------|------|-------|-------------|
|
|
| `apply` | signal | Actions | Apply optimistic update |
|
|
| `commit` | signal | Actions | Confirm update succeeded |
|
|
| `rollback` | signal | Actions | Revert update |
|
|
| `optimisticValue` | * | Update | Value to apply optimistically |
|
|
| `storeName` | string | Store | Global store name (if using store) |
|
|
| `key` | string | Store | Store key to update |
|
|
| `variableName` | string | Variable | Or Variable node to update |
|
|
| `transactionId` | string | Transaction | Unique ID for this update |
|
|
| `timeout` | number | Config | Auto-rollback after ms (default: 30000) |
|
|
|
|
#### Outputs
|
|
|
|
| Port Name | Type | Group | Description |
|
|
|-----------|------|-------|-------------|
|
|
| `value` | * | Data | Current value (optimistic or committed) |
|
|
| `isPending` | boolean | Status | Update awaiting confirmation |
|
|
| `isCommitted` | boolean | Status | Update confirmed |
|
|
| `isRolledBack` | boolean | Status | Update reverted |
|
|
| `applied` | signal | Events | Fires after optimistic apply |
|
|
| `committed` | signal | Events | Fires after commit |
|
|
| `rolledBack` | signal | Events | Fires after rollback |
|
|
| `timedOut` | signal | Events | Fires if timeout reached |
|
|
| `previousValue` | * | Data | Value before optimistic update |
|
|
| `error` | string | Events | Error message on rollback |
|
|
|
|
### State Machine
|
|
|
|
```
|
|
┌─────────┐
|
|
START │ │
|
|
────┬─→│ IDLE │←──────────┐
|
|
│ │ │ │
|
|
│ └─────────┘ │
|
|
│ │ │
|
|
│ [apply] │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────┐ [commit]
|
|
│ │ PENDING │───────────┘
|
|
│ │ │
|
|
│ └─────────┘
|
|
│ │
|
|
│ [rollback]
|
|
│ [timeout]
|
|
│ │
|
|
│ ▼
|
|
│ ┌─────────┐
|
|
└──│ROLLED │
|
|
│BACK │
|
|
└─────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Details
|
|
|
|
### File Structure
|
|
|
|
```
|
|
packages/noodl-runtime/src/nodes/std-library/data/
|
|
├── optimisticupdatenode.js # Main node
|
|
├── optimisticmanager.js # Transaction manager
|
|
└── optimisticupdate.test.js # Tests
|
|
```
|
|
|
|
### Transaction Manager
|
|
|
|
```javascript
|
|
// optimisticmanager.js
|
|
|
|
class OptimisticUpdateManager {
|
|
constructor() {
|
|
this.transactions = new Map();
|
|
}
|
|
|
|
/**
|
|
* Apply optimistic update
|
|
*/
|
|
apply(transactionId, currentValue, optimisticValue, options = {}) {
|
|
if (this.transactions.has(transactionId)) {
|
|
throw new Error(`Transaction ${transactionId} already exists`);
|
|
}
|
|
|
|
const transaction = {
|
|
id: transactionId,
|
|
previousValue: currentValue,
|
|
optimisticValue: optimisticValue,
|
|
appliedAt: Date.now(),
|
|
status: 'pending',
|
|
timeout: options.timeout || 30000,
|
|
timer: null
|
|
};
|
|
|
|
// Set timeout for auto-rollback
|
|
if (transaction.timeout > 0) {
|
|
transaction.timer = setTimeout(() => {
|
|
this.timeout(transactionId);
|
|
}, transaction.timeout);
|
|
}
|
|
|
|
this.transactions.set(transactionId, transaction);
|
|
|
|
console.log(`[OptimisticUpdate] Applied: ${transactionId}`);
|
|
|
|
return {
|
|
value: optimisticValue,
|
|
isPending: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Commit transaction (success)
|
|
*/
|
|
commit(transactionId) {
|
|
const transaction = this.transactions.get(transactionId);
|
|
if (!transaction) {
|
|
console.warn(`[OptimisticUpdate] Transaction not found: ${transactionId}`);
|
|
return null;
|
|
}
|
|
|
|
// Clear timeout
|
|
if (transaction.timer) {
|
|
clearTimeout(transaction.timer);
|
|
}
|
|
|
|
transaction.status = 'committed';
|
|
this.transactions.delete(transactionId);
|
|
|
|
console.log(`[OptimisticUpdate] Committed: ${transactionId}`);
|
|
|
|
return {
|
|
value: transaction.optimisticValue,
|
|
isPending: false,
|
|
isCommitted: true
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Rollback transaction (failure)
|
|
*/
|
|
rollback(transactionId, error = null) {
|
|
const transaction = this.transactions.get(transactionId);
|
|
if (!transaction) {
|
|
console.warn(`[OptimisticUpdate] Transaction not found: ${transactionId}`);
|
|
return null;
|
|
}
|
|
|
|
// Clear timeout
|
|
if (transaction.timer) {
|
|
clearTimeout(transaction.timer);
|
|
}
|
|
|
|
transaction.status = 'rolled_back';
|
|
transaction.error = error;
|
|
|
|
const result = {
|
|
value: transaction.previousValue,
|
|
isPending: false,
|
|
isRolledBack: true,
|
|
error: error
|
|
};
|
|
|
|
this.transactions.delete(transactionId);
|
|
|
|
console.log(`[OptimisticUpdate] Rolled back: ${transactionId}`, error);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Auto-rollback on timeout
|
|
*/
|
|
timeout(transactionId) {
|
|
const transaction = this.transactions.get(transactionId);
|
|
if (!transaction) return null;
|
|
|
|
console.warn(`[OptimisticUpdate] Timeout: ${transactionId}`);
|
|
|
|
return this.rollback(transactionId, 'Request timed out');
|
|
}
|
|
|
|
/**
|
|
* Check if transaction is pending
|
|
*/
|
|
isPending(transactionId) {
|
|
const transaction = this.transactions.get(transactionId);
|
|
return transaction && transaction.status === 'pending';
|
|
}
|
|
|
|
/**
|
|
* Get transaction info
|
|
*/
|
|
getTransaction(transactionId) {
|
|
return this.transactions.get(transactionId);
|
|
}
|
|
|
|
/**
|
|
* Clear all transactions (cleanup)
|
|
*/
|
|
clear() {
|
|
this.transactions.forEach(transaction => {
|
|
if (transaction.timer) {
|
|
clearTimeout(transaction.timer);
|
|
}
|
|
});
|
|
this.transactions.clear();
|
|
}
|
|
}
|
|
|
|
const optimisticUpdateManager = new OptimisticUpdateManager();
|
|
|
|
module.exports = { optimisticUpdateManager };
|
|
```
|
|
|
|
### Optimistic Update Node
|
|
|
|
```javascript
|
|
// optimisticupdatenode.js
|
|
|
|
const { optimisticUpdateManager } = require('./optimisticmanager');
|
|
const { globalStoreManager } = require('./globalstore');
|
|
|
|
var OptimisticUpdateNode = {
|
|
name: 'net.noodl.OptimisticUpdate',
|
|
displayNodeName: 'Optimistic Update',
|
|
category: 'Data',
|
|
color: 'orange',
|
|
|
|
initialize: function() {
|
|
this._internal.transactionId = null;
|
|
this._internal.currentValue = null;
|
|
this._internal.isPending = false;
|
|
},
|
|
|
|
inputs: {
|
|
apply: {
|
|
type: 'signal',
|
|
displayName: 'Apply',
|
|
group: 'Actions',
|
|
valueChangedToTrue: function() {
|
|
this.doApply();
|
|
}
|
|
},
|
|
|
|
commit: {
|
|
type: 'signal',
|
|
displayName: 'Commit',
|
|
group: 'Actions',
|
|
valueChangedToTrue: function() {
|
|
this.doCommit();
|
|
}
|
|
},
|
|
|
|
rollback: {
|
|
type: 'signal',
|
|
displayName: 'Rollback',
|
|
group: 'Actions',
|
|
valueChangedToTrue: function() {
|
|
this.doRollback();
|
|
}
|
|
},
|
|
|
|
optimisticValue: {
|
|
type: '*',
|
|
displayName: 'Optimistic Value',
|
|
group: 'Update',
|
|
set: function(value) {
|
|
this._internal.optimisticValue = value;
|
|
}
|
|
},
|
|
|
|
storeName: {
|
|
type: 'string',
|
|
displayName: 'Store Name',
|
|
group: 'Store',
|
|
set: function(value) {
|
|
this._internal.storeName = value;
|
|
}
|
|
},
|
|
|
|
key: {
|
|
type: 'string',
|
|
displayName: 'Key',
|
|
group: 'Store',
|
|
set: function(value) {
|
|
this._internal.key = value;
|
|
}
|
|
},
|
|
|
|
transactionId: {
|
|
type: 'string',
|
|
displayName: 'Transaction ID',
|
|
group: 'Transaction',
|
|
set: function(value) {
|
|
this._internal.transactionId = value;
|
|
}
|
|
},
|
|
|
|
timeout: {
|
|
type: 'number',
|
|
displayName: 'Timeout (ms)',
|
|
group: 'Config',
|
|
default: 30000,
|
|
set: function(value) {
|
|
this._internal.timeout = value;
|
|
}
|
|
}
|
|
},
|
|
|
|
outputs: {
|
|
value: {
|
|
type: '*',
|
|
displayName: 'Value',
|
|
group: 'Data',
|
|
getter: function() {
|
|
return this._internal.currentValue;
|
|
}
|
|
},
|
|
|
|
isPending: {
|
|
type: 'boolean',
|
|
displayName: 'Is Pending',
|
|
group: 'Status',
|
|
getter: function() {
|
|
return this._internal.isPending;
|
|
}
|
|
},
|
|
|
|
isCommitted: {
|
|
type: 'boolean',
|
|
displayName: 'Is Committed',
|
|
group: 'Status',
|
|
getter: function() {
|
|
return this._internal.isCommitted;
|
|
}
|
|
},
|
|
|
|
isRolledBack: {
|
|
type: 'boolean',
|
|
displayName: 'Is Rolled Back',
|
|
group: 'Status',
|
|
getter: function() {
|
|
return this._internal.isRolledBack;
|
|
}
|
|
},
|
|
|
|
applied: {
|
|
type: 'signal',
|
|
displayName: 'Applied',
|
|
group: 'Events'
|
|
},
|
|
|
|
committed: {
|
|
type: 'signal',
|
|
displayName: 'Committed',
|
|
group: 'Events'
|
|
},
|
|
|
|
rolledBack: {
|
|
type: 'signal',
|
|
displayName: 'Rolled Back',
|
|
group: 'Events'
|
|
},
|
|
|
|
timedOut: {
|
|
type: 'signal',
|
|
displayName: 'Timed Out',
|
|
group: 'Events'
|
|
},
|
|
|
|
previousValue: {
|
|
type: '*',
|
|
displayName: 'Previous Value',
|
|
group: 'Data',
|
|
getter: function() {
|
|
return this._internal.previousValue;
|
|
}
|
|
},
|
|
|
|
error: {
|
|
type: 'string',
|
|
displayName: 'Error',
|
|
group: 'Events',
|
|
getter: function() {
|
|
return this._internal.error;
|
|
}
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
doApply: function() {
|
|
const transactionId = this._internal.transactionId || this.generateTransactionId();
|
|
const optimisticValue = this._internal.optimisticValue;
|
|
|
|
// Get current value from store
|
|
let currentValue;
|
|
if (this._internal.storeName && this._internal.key) {
|
|
const store = globalStoreManager.getState(this._internal.storeName);
|
|
currentValue = store[this._internal.key];
|
|
} else {
|
|
currentValue = this._internal.currentValue;
|
|
}
|
|
|
|
// Apply optimistic update
|
|
const result = optimisticUpdateManager.apply(
|
|
transactionId,
|
|
currentValue,
|
|
optimisticValue,
|
|
{ timeout: this._internal.timeout }
|
|
);
|
|
|
|
// Update store if configured
|
|
if (this._internal.storeName && this._internal.key) {
|
|
globalStoreManager.setKey(
|
|
this._internal.storeName,
|
|
this._internal.key,
|
|
optimisticValue
|
|
);
|
|
}
|
|
|
|
// Update internal state
|
|
this._internal.previousValue = currentValue;
|
|
this._internal.currentValue = optimisticValue;
|
|
this._internal.isPending = true;
|
|
this._internal.isCommitted = false;
|
|
this._internal.isRolledBack = false;
|
|
this._internal.transactionId = transactionId;
|
|
|
|
this.flagOutputDirty('value');
|
|
this.flagOutputDirty('isPending');
|
|
this.flagOutputDirty('previousValue');
|
|
this.sendSignalOnOutput('applied');
|
|
},
|
|
|
|
doCommit: function() {
|
|
const transactionId = this._internal.transactionId;
|
|
if (!transactionId) {
|
|
console.warn('[OptimisticUpdate] No transaction to commit');
|
|
return;
|
|
}
|
|
|
|
const result = optimisticUpdateManager.commit(transactionId);
|
|
if (!result) return;
|
|
|
|
this._internal.isPending = false;
|
|
this._internal.isCommitted = true;
|
|
|
|
this.flagOutputDirty('isPending');
|
|
this.flagOutputDirty('isCommitted');
|
|
this.sendSignalOnOutput('committed');
|
|
},
|
|
|
|
doRollback: function(error = null) {
|
|
const transactionId = this._internal.transactionId;
|
|
if (!transactionId) {
|
|
console.warn('[OptimisticUpdate] No transaction to rollback');
|
|
return;
|
|
}
|
|
|
|
const result = optimisticUpdateManager.rollback(transactionId, error);
|
|
if (!result) return;
|
|
|
|
// Revert store if configured
|
|
if (this._internal.storeName && this._internal.key) {
|
|
globalStoreManager.setKey(
|
|
this._internal.storeName,
|
|
this._internal.key,
|
|
result.value
|
|
);
|
|
}
|
|
|
|
this._internal.currentValue = result.value;
|
|
this._internal.isPending = false;
|
|
this._internal.isRolledBack = true;
|
|
this._internal.error = result.error;
|
|
|
|
this.flagOutputDirty('value');
|
|
this.flagOutputDirty('isPending');
|
|
this.flagOutputDirty('isRolledBack');
|
|
this.flagOutputDirty('error');
|
|
this.sendSignalOnOutput('rolledBack');
|
|
|
|
if (result.error === 'Request timed out') {
|
|
this.sendSignalOnOutput('timedOut');
|
|
}
|
|
},
|
|
|
|
generateTransactionId: function() {
|
|
return 'tx_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
},
|
|
|
|
_onNodeDeleted: function() {
|
|
// Clean up pending transaction
|
|
if (this._internal.transactionId) {
|
|
optimisticUpdateManager.rollback(this._internal.transactionId);
|
|
}
|
|
}
|
|
},
|
|
|
|
getInspectInfo: function() {
|
|
return {
|
|
type: 'value',
|
|
value: {
|
|
status: this._internal.isPending ? 'Pending' : 'Idle',
|
|
value: this._internal.currentValue,
|
|
transactionId: this._internal.transactionId
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
node: OptimisticUpdateNode
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Usage Examples
|
|
|
|
### Example 1: Accept Connection (Erleah)
|
|
|
|
```
|
|
[Button: "Accept"] clicked
|
|
↓
|
|
[Optimistic Update]
|
|
optimisticValue: { status: "accepted" }
|
|
storeName: "connections"
|
|
key: "connection-{id}"
|
|
timeout: 5000
|
|
↓
|
|
[Optimistic Update] apply signal
|
|
↓
|
|
[HTTP Request] POST /connections/{id}/accept
|
|
↓
|
|
[HTTP Request] success
|
|
→ [Optimistic Update] commit
|
|
|
|
[HTTP Request] failure
|
|
→ [Optimistic Update] rollback
|
|
→ [Show Toast] "Failed to accept connection"
|
|
```
|
|
|
|
### Example 2: Add to Timeline
|
|
|
|
```
|
|
[AI Agent] suggests session
|
|
↓
|
|
[Button: "Add to Timeline"] clicked
|
|
↓
|
|
[Optimistic Update]
|
|
optimisticValue: { ...sessionData, id: tempId }
|
|
storeName: "agenda"
|
|
key: "timeline"
|
|
↓
|
|
[Optimistic Update] value
|
|
→ [Array] push to timeline array
|
|
→ [Optimistic Update] optimisticValue
|
|
→ [Optimistic Update] apply
|
|
|
|
// Timeline immediately shows item!
|
|
|
|
[HTTP Request] POST /agenda/sessions
|
|
→ returns real session ID
|
|
↓
|
|
[Success]
|
|
→ [Replace temp ID with real ID]
|
|
→ [Optimistic Update] commit
|
|
|
|
[Failure]
|
|
→ [Optimistic Update] rollback
|
|
→ [Show Error] "Could not add session"
|
|
```
|
|
|
|
### Example 3: Chat Message Send
|
|
|
|
```
|
|
[Text Input] → messageText
|
|
[Send Button] clicked
|
|
↓
|
|
[Object] create message
|
|
id: tempId
|
|
text: messageText
|
|
status: "sending"
|
|
timestamp: now
|
|
↓
|
|
[Optimistic Update]
|
|
storeName: "chat"
|
|
key: "messages"
|
|
↓
|
|
[Optimistic Update] value (current messages)
|
|
→ [Array] push new message
|
|
→ [Optimistic Update] optimisticValue
|
|
→ [Optimistic Update] apply
|
|
|
|
// Message appears immediately with "sending" indicator
|
|
|
|
[HTTP Request] POST /messages
|
|
↓
|
|
[Success] → real message from server
|
|
→ [Update message status to "sent"]
|
|
→ [Optimistic Update] commit
|
|
|
|
[Failure]
|
|
→ [Optimistic Update] rollback
|
|
→ [Update message status to "failed"]
|
|
→ [Show Retry Button]
|
|
```
|
|
|
|
### Example 4: Toggle Bookmark
|
|
|
|
```
|
|
[Star Icon] clicked
|
|
↓
|
|
[Variable: isBookmarked] current value
|
|
→ [Expression] !value (toggle)
|
|
→ [Optimistic Update] optimisticValue
|
|
↓
|
|
[Optimistic Update] apply
|
|
↓
|
|
[Star Icon] filled = [Optimistic Update] value
|
|
|
|
// Star fills immediately
|
|
|
|
[HTTP Request] POST /bookmarks
|
|
↓
|
|
[Success]
|
|
→ [Optimistic Update] commit
|
|
|
|
[Failure]
|
|
→ [Optimistic Update] rollback
|
|
→ [Show Toast] "Couldn't save bookmark"
|
|
|
|
// Star unfills on failure
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
### Functional Tests
|
|
|
|
- [ ] Apply sets value optimistically
|
|
- [ ] Commit keeps optimistic value
|
|
- [ ] Rollback reverts to previous value
|
|
- [ ] Timeout triggers automatic rollback
|
|
- [ ] isPending reflects correct state
|
|
- [ ] Store integration works
|
|
- [ ] Multiple transactions don't interfere
|
|
- [ ] Transaction IDs are unique
|
|
- [ ] Signals fire at correct times
|
|
|
|
### Edge Cases
|
|
|
|
- [ ] Commit without apply (no-op)
|
|
- [ ] Rollback without apply (no-op)
|
|
- [ ] Apply twice with same transaction ID (error)
|
|
- [ ] Commit after timeout (no-op)
|
|
- [ ] Very fast success (commit before timeout)
|
|
- [ ] Network reconnect scenarios
|
|
- [ ] Component unmount cleans up transaction
|
|
|
|
### Performance
|
|
|
|
- [ ] No memory leaks with many transactions
|
|
- [ ] Timeout cleanup works correctly
|
|
- [ ] Multiple optimistic updates in quick succession
|
|
|
|
---
|
|
|
|
## Documentation Requirements
|
|
|
|
### User-Facing Docs
|
|
|
|
Create: `docs/nodes/data/optimistic-update.md`
|
|
|
|
```markdown
|
|
# Optimistic Update
|
|
|
|
Make your UI feel instant by updating immediately, then confirming with the server later. Perfect for actions like liking, bookmarking, or accepting requests.
|
|
|
|
## The Problem
|
|
|
|
Without optimistic updates:
|
|
```
|
|
User clicks button → Show spinner → Wait... → Update UI
|
|
(feels slow)
|
|
```
|
|
|
|
With optimistic updates:
|
|
```
|
|
User clicks button → Update UI immediately → Confirm in background
|
|
(feels instant!)
|
|
```
|
|
|
|
## Basic Pattern
|
|
|
|
1. Apply optimistic update (UI changes immediately)
|
|
2. Send request to server (background)
|
|
3. If success: Commit (keep the change)
|
|
4. If failure: Rollback (undo the change)
|
|
|
|
## Example: Like Button
|
|
|
|
[Full example with visual diagrams]
|
|
|
|
## With Global Store
|
|
|
|
For shared state, use with Global Store:
|
|
|
|
```
|
|
[Optimistic Update]
|
|
storeName: "posts"
|
|
key: "likes"
|
|
optimisticValue: likesCount + 1
|
|
```
|
|
|
|
All components subscribing to the store update automatically!
|
|
|
|
## Timeout
|
|
|
|
If server doesn't respond:
|
|
```
|
|
[Optimistic Update]
|
|
timeout: 5000 // Auto-rollback after 5s
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Always handle rollback**: Show error message
|
|
2. **Show pending state**: "Saving..." indicator (optional)
|
|
3. **Use unique IDs**: Let node generate, or provide your own
|
|
4. **Set reasonable timeout**: 5-30 seconds depending on operation
|
|
```
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
1. ✅ Optimistic updates apply immediately
|
|
2. ✅ Rollback works on failure
|
|
3. ✅ Timeout prevents stuck pending states
|
|
4. ✅ Integrates with Global Store
|
|
5. ✅ No memory leaks
|
|
6. ✅ Clear documentation with examples
|
|
7. ✅ Works in Erleah for responsive interactions
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
1. **Retry Logic** - Auto-retry failed operations
|
|
2. **Conflict Resolution** - Handle concurrent updates
|
|
3. **Offline Queue** - Queue updates when offline
|
|
4. **Animation Hooks** - Smooth transitions on rollback
|
|
5. **Batch Commits** - Commit multiple related transactions
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [Optimistic UI](https://www.apollographql.com/docs/react/performance/optimistic-ui/) - Apollo GraphQL docs
|
|
- [React Query Optimistic Updates](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates)
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
- AGENT-003 (Global State Store) - for store integration
|
|
|
|
## Blocked By
|
|
|
|
- AGENT-003
|
|
|
|
## Blocks
|
|
|
|
- None (optional enhancement for Erleah)
|
|
|
|
---
|
|
|
|
## Estimated Effort Breakdown
|
|
|
|
| Phase | Estimate | Description |
|
|
|-------|----------|-------------|
|
|
| Transaction Manager | 0.5 day | Core state machine |
|
|
| Optimistic Update Node | 1 day | Main node with store integration |
|
|
| Testing | 0.5 day | Unit tests, edge cases |
|
|
| Documentation | 0.5 day | User docs, examples |
|
|
|
|
**Total: 2.5 days**
|
|
|
|
Buffer: +0.5 day for edge cases = **3 days**
|
|
|
|
**Final: 2-3 days**
|