flarp
v2.5.1
Published
DOM-native XML state management with multi-master sync
Maintainers
Readme
Flarp
DOM-native XML State Management with Multi-Master Sync
Flarp treats XML as state, the DOM as the runtime, and all browser tabs as equal peers. State survives refresh, syncs across tabs, and conflicts are resolved automatically.
<f-store key="myapp" autosave="500">
<user>
<n>Alice</n>
<role>admin</role>
</user>
</f-store>
<f-text path="user.name"></f-text>
<f-field path="user.role"></f-field>Core Principles
- XML is State — Human-readable, self-describing, works with AI
- DOM is Runtime — No virtual DOM, no compile step, just the browser
- Multi-Master Sync — All tabs are equal peers, no single source of truth
- Eventually Consistent — Conflicts resolved deterministically, all tabs agree
Quick Start
<!DOCTYPE html>
<html>
<head>
<script type="module" src="./src/index.js"></script>
</head>
<body>
<f-store key="demo" autosave="500">
<counter>
<value>0</value>
</counter>
</f-store>
<p>Count: <f-text path="counter.value"></f-text></p>
<button id="inc">+1</button>
<script type="module">
const store = document.querySelector('f-store');
store.state.when('ready', () => {
document.getElementById('inc').onclick = () => {
const val = store.at('counter.value');
val.value = String(Number(val.value) + 1);
};
});
</script>
</body>
</html>Revision System (CouchDB-style)
Every node has two special attributes:
- uuid — Stable identity (never changes)
- rev — Version string:
{number}-{hash}
<user uuid="abc123" rev="3-f7a8b9c0">
<n>Alice</n>
</user>Why This Format?
The revision number enables quick comparison (higher wins). The hash enables conflict detection (same number, different hash = conflict) and deterministic tie-breaking (alphabetically first hash wins).
Conflict Resolution
When two tabs write simultaneously:
Tab A writes: rev="3-aaa111"
Tab B writes: rev="3-bbb222"- Both writes succeed (no data loss!)
- Conflict detected (same number: 3)
- Tab A wins (aaa < bbb alphabetically)
- All tabs independently agree on Tab A
// Listen for conflicts
store.onConflict(({ uuid, localRev, remoteRev, winner }) => {
console.log(`Conflict on ${uuid}: ${winner} won`);
});Cross-Tab Synchronization
When you set key="...", Flarp automatically:
- Persists to localStorage
- Syncs across browser tabs via BroadcastChannel
- Requests catch-up sync on connect
<f-store key="myapp" autosave="500">
<!-- Shared across all tabs with key="myapp" -->
</f-store>How It Works
- Each tab broadcasts changes to a shared channel
- Other tabs receive and apply changes (if revision wins)
- Changes include:
{ uuid, rev, data: "<xml>..." } - On startup, tabs request missed changes from peers
// Access the sync instance
store.sync.onChange(change => {
console.log('Remote change:', change);
});
store.sync.onConflict(info => {
console.log('Conflict:', info);
});External Updates (Server Push)
Apply updates from WebSocket, HTTP push, or postMessage:
// From WebSocket
ws.onmessage = e => {
const { applied, conflict, winner } = store.applyRemote(e.data);
console.log(`Applied: ${applied}, Winner: ${winner}`);
};
// From postMessage
window.onmessage = e => {
if (e.data.type === 'node-update') {
store.applyRemote(e.data.xml);
}
};The XML must include uuid and rev:
<n uuid="abc123" rev="5-xyz789">New Value</n>Components
<f-store> — State Container
<f-store
key="myapp" <!-- Storage/sync key -->
autosave="500" <!-- Debounced save (ms) -->
sync="true" <!-- Enable cross-tab sync -->
>
<!-- Your XML state here -->
</f-store><f-text> — Display Value
<f-text path="user.name"></f-text><f-field> — Two-Way Binding
<f-field path="user.name"></f-field>
<f-field path="user.role" type="select">
<option value="admin">Admin</option>
<option value="user">User</option>
</f-field><f-when> — Conditional
<f-when test="user.active == 'true'">
<span>Active!</span>
</f-when>
<f-when test="cart.total > 100">
<span>Free shipping!</span>
</f-when><f-match> — Switch
<f-match test="user.role">
<f-case value="admin">Admin Panel</f-case>
<f-case value="user">Dashboard</f-case>
<f-else>Please log in</f-else>
</f-match><f-each> — Iteration
<f-each path="users.user">
<template>
<div class="user">
<f-text path="name"></f-text>
</div>
</template>
</f-each>JavaScript API
const store = document.querySelector('f-store');
// Wait for ready
store.state.when('ready', () => {
// Get reactive node
const name = store.at('user.name');
// Read/write
console.log(name.value);
name.value = 'Bob';
// Subscribe to changes
name.subscribe(v => console.log('Name changed:', v));
// Query multiple
const users = store.query('users.user');
// Add node
store.add('users', '<user><n>New User</n></user>');
// Remove node
store.remove('users.user[0]');
// Serialize
const xml = store.serialize();
// Manual save
store.save();
// Apply external update
store.applyRemote('<n uuid="..." rev="5-abc">Value</n>');
});
// Event handlers
store.onChange(xml => console.log('Changed'));
store.onConflict(info => console.log('Conflict:', info));Architecture
flarp/
├── src/
│ ├── core/ # Reactive primitives
│ │ ├── Signal.js # Synchronous reactive value
│ │ └── State.js # Named states (ready, synced, etc.)
│ │
│ ├── xml/ # XML utilities
│ │ ├── Node.js # Reactive node wrapper
│ │ ├── Path.js # Path resolution
│ │ └── Tree.js # Tree management
│ │
│ ├── sync/ # Persistence & sync
│ │ ├── Sync.js # Cross-tab sync with changes feed
│ │ ├── Store.js # FStore component
│ │ ├── Persist.js # Storage adapters
│ │ └── Channel.js # BroadcastChannel wrapper
│ │
│ ├── dom/ # DOM utilities
│ │ └── find.js # Store discovery
│ │
│ ├── components/ # Web Components
│ │ ├── FText.js
│ │ ├── FField.js
│ │ ├── FWhen.js
│ │ ├── FEach.js
│ │ └── FBind.js
│ │
│ └── index.js # Main exports
│
├── index.html # Demo
├── multiuser.html # Multi-tab sync demo
└── README.mdMulti-User Demo
Open multiuser.html in multiple browser tabs to see sync in action:
- Each tab gets a unique user ID
- Changes sync instantly across all tabs
- Conflicts are detected and resolved automatically
- Enable "auto-write" to stress test
# Start a local server
npm run dev
# Open multiple tabs to:
http://localhost:8080/multiuser.htmlSaving & Persistence
Automatic
<f-store key="myapp" autosave="500">
<!-- Saves 500ms after last change -->
</f-store>Manual
store.save(); // Save now
store.clear(); // Clear stored stateEmergency Save
Flarp automatically saves on beforeunload (browser close/refresh).
Storage Location
Data is stored in localStorage under flarp-data:{key}.
Tag Naming
⚠️ Use lowercase tags! The DOM lowercases all tag names, so <User> becomes <user>. For clarity, always write lowercase:
<!-- Good -->
<f-store>
<user><n>Alice</n></user>
</f-store>
<!-- Works but confusing -->
<f-store>
<User><n>Alice</n></User>
</f-store>Browser Support
- Modern browsers with BroadcastChannel (Chrome, Firefox, Safari, Edge)
- Falls back to localStorage events for older browsers
- Requires ES modules support
Name
flarp (from Jargon File)
/flarp/ [Rutgers University] Yet another metasyntactic variable (see foo). Among those who use it, it is associated with a legend that any program not containing the word "flarp" somewhere will not work. The legend is discreetly silent on the reliability of programs which do contain the magic word.
License
MIT
