flarp 2.0.1 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,145 +1,203 @@
1
1
  # Flarp
2
2
 
3
- **DOM-native XML State Management for Web Components**
3
+ **DOM-native XML State Management with Multi-Master Sync**
4
4
 
5
+ 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.
6
+
7
+ ```html
8
+ <f-store key="myapp" autosave="500">
9
+ <user>
10
+ <n>Alice</n>
11
+ <role>admin</role>
12
+ </user>
13
+ </f-store>
14
+
15
+ <f-text path="user.name"></f-text>
16
+ <f-field path="user.role"></f-field>
5
17
  ```
6
- XML is state. Signals are reactive. The DOM is the runtime.
18
+
19
+ ## Core Principles
20
+
21
+ 1. **XML is State** — Human-readable, self-describing, works with AI
22
+ 2. **DOM is Runtime** — No virtual DOM, no compile step, just the browser
23
+ 3. **Multi-Master Sync** — All tabs are equal peers, no single source of truth
24
+ 4. **Eventually Consistent** — Conflicts resolved deterministically, all tabs agree
25
+
26
+ ---
27
+
28
+ ## Quick Start
29
+
30
+ ```html
31
+ <!DOCTYPE html>
32
+ <html>
33
+ <head>
34
+ <script type="module" src="./src/index.js"></script>
35
+ </head>
36
+ <body>
37
+ <f-store key="demo" autosave="500">
38
+ <counter>
39
+ <value>0</value>
40
+ </counter>
41
+ </f-store>
42
+
43
+ <p>Count: <f-text path="counter.value"></f-text></p>
44
+ <button id="inc">+1</button>
45
+
46
+ <script type="module">
47
+ const store = document.querySelector('f-store');
48
+
49
+ store.state.when('ready', () => {
50
+ document.getElementById('inc').onclick = () => {
51
+ const val = store.at('counter.value');
52
+ val.value = String(Number(val.value) + 1);
53
+ };
54
+ });
55
+ </script>
56
+ </body>
57
+ </html>
7
58
  ```
8
59
 
9
60
  ---
10
61
 
11
- ## Why Flarp?
62
+ ## Revision System (CouchDB-style)
12
63
 
13
- State management shouldn't require learning proprietary patterns. Flarp uses what the web already has:
64
+ Every node has two special attributes:
14
65
 
15
- - **XML for state** — Nested structure you can see and understand
16
- - **Signals for reactivity** — No virtual DOM, no diffing, just updates
17
- - **Web Components for UI** — Standard, portable, composable
18
- - **DOM as runtime** — MutationObserver does the scheduling
66
+ - **uuid** — Stable identity (never changes)
67
+ - **rev** — Version string: `{number}-{hash}`
19
68
 
20
- ## Quick Start
69
+ ```xml
70
+ <user uuid="abc123" rev="3-f7a8b9c0">
71
+ <n>Alice</n>
72
+ </user>
73
+ ```
21
74
 
22
- ```html
23
- <script type="module">
24
- import 'https://unpkg.com/flarp/src/index.js';
25
- </script>
75
+ ### Why This Format?
26
76
 
27
- <f-state key="myapp" autosave="500">
28
- <user>
29
- <name>Alice</name>
30
- <role>Developer</role>
31
- </user>
32
- </f-state>
77
+ 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).
78
+
79
+ ### Conflict Resolution
33
80
 
34
- <main>
35
- <h1><f-text path="user.name"></f-text></h1>
36
- <f-field path="user.role"></f-field>
37
- </main>
81
+ When two tabs write simultaneously:
82
+
83
+ ```
84
+ Tab A writes: rev="3-aaa111"
85
+ Tab B writes: rev="3-bbb222"
38
86
  ```
39
87
 
40
- Components find `<f-state>` automatically no nesting required!
88
+ 1. Both writes succeed (no data loss!)
89
+ 2. Conflict detected (same number: 3)
90
+ 3. Tab A wins (aaa < bbb alphabetically)
91
+ 4. All tabs independently agree on Tab A
41
92
 
42
- > **Note:** The DOM lowercases all tag names. Use lowercase paths like `user.name`, not `User.Name`. Path matching is case-insensitive, so `User.Name` will still work, but your XML will appear lowercase in the DOM.
93
+ ```javascript
94
+ // Listen for conflicts
95
+ store.onConflict(({ uuid, localRev, remoteRev, winner }) => {
96
+ console.log(`Conflict on ${uuid}: ${winner} won`);
97
+ });
98
+ ```
43
99
 
44
- ## Key Innovation: Signal-Based Readiness
100
+ ---
45
101
 
46
- Events fire once. If you miss them, you miss them. Signals don't have this problem:
102
+ ## Cross-Tab Synchronization
47
103
 
48
- ```js
49
- // This works even if 'ready' already happened!
50
- store.state.when('ready', () => {
51
- const name = store.at('User.Name');
52
- name.subscribe(v => console.log('Name:', v));
104
+ When you set `key="..."`, Flarp automatically:
105
+
106
+ 1. Persists to localStorage
107
+ 2. Syncs across browser tabs via BroadcastChannel
108
+ 3. Requests catch-up sync on connect
109
+
110
+ ```html
111
+ <f-store key="myapp" autosave="500">
112
+ <!-- Shared across all tabs with key="myapp" -->
113
+ </f-store>
114
+ ```
115
+
116
+ ### How It Works
117
+
118
+ 1. Each tab broadcasts changes to a shared channel
119
+ 2. Other tabs receive and apply changes (if revision wins)
120
+ 3. Changes include: `{ uuid, rev, data: "<xml>..." }`
121
+ 4. On startup, tabs request missed changes from peers
122
+
123
+ ```javascript
124
+ // Access the sync instance
125
+ store.sync.onChange(change => {
126
+ console.log('Remote change:', change);
53
127
  });
54
128
 
55
- // Or use async/await
56
- await store.state.until('ready');
129
+ store.sync.onConflict(info => {
130
+ console.log('Conflict:', info);
131
+ });
57
132
  ```
58
133
 
59
- ## Installation
134
+ ---
60
135
 
61
- ```bash
62
- npm install flarp
136
+ ## External Updates (Server Push)
137
+
138
+ Apply updates from WebSocket, HTTP push, or postMessage:
139
+
140
+ ```javascript
141
+ // From WebSocket
142
+ ws.onmessage = e => {
143
+ const { applied, conflict, winner } = store.applyRemote(e.data);
144
+ console.log(`Applied: ${applied}, Winner: ${winner}`);
145
+ };
146
+
147
+ // From postMessage
148
+ window.onmessage = e => {
149
+ if (e.data.type === 'node-update') {
150
+ store.applyRemote(e.data.xml);
151
+ }
152
+ };
63
153
  ```
64
154
 
65
- Or use directly:
155
+ The XML must include uuid and rev:
66
156
 
67
- ```html
68
- <script type="module">
69
- import 'https://unpkg.com/flarp/src/index.js';
70
- </script>
157
+ ```xml
158
+ <n uuid="abc123" rev="5-xyz789">New Value</n>
71
159
  ```
72
160
 
73
161
  ---
74
162
 
75
163
  ## Components
76
164
 
77
- ### `<f-state>` — The Store
165
+ ### `<f-store>` — State Container
78
166
 
79
167
  ```html
80
- <f-state
81
- key="myapp" <!-- Persistence key -->
82
- autosave="500" <!-- Debounce ms -->
83
- src="/api/data" <!-- Load from URL -->
168
+ <f-store
169
+ key="myapp" <!-- Storage/sync key -->
170
+ autosave="500" <!-- Debounced save (ms) -->
171
+ sync="true" <!-- Enable cross-tab sync -->
84
172
  >
85
- <User>
86
- <n>Alice</n>
87
- </User>
88
- </f-state>
173
+ <!-- Your XML state here -->
174
+ </f-store>
89
175
  ```
90
176
 
91
- **API:**
92
- - `at(path)` — Get reactive node
93
- - `query(path)` — Get all matching nodes
94
- - `set(path, value)` — Set value
95
- - `add(path, xml)` — Add child
96
- - `remove(path)` — Remove node
97
- - `serialize()` — Get XML string
98
- - `on(action, fn)` — Register action
99
- - `emit(action, payload)` — Dispatch action
100
- - `state.when(name, fn)` — React to state
101
- - `state.until(name)` — Promise for state
102
-
103
- ### `<f-text>` — Display
177
+ ### `<f-text>` — Display Value
104
178
 
105
179
  ```html
106
- <f-text path="User.Name"></f-text>
107
- <f-text path="Price" format="currency"></f-text>
180
+ <f-text path="user.name"></f-text>
108
181
  ```
109
182
 
110
- **Formats:** `uppercase`, `lowercase`, `number`, `currency`, `percent`
111
-
112
183
  ### `<f-field>` — Two-Way Binding
113
184
 
114
185
  ```html
115
- <f-field path="User.Name"></f-field>
116
- <f-field path="User.Age" type="number"></f-field>
117
- <f-field path="User.Active" type="checkbox"></f-field>
118
- ```
119
-
120
- ### `<f-each>` — Lists
121
-
122
- ```html
123
- <f-each path="Users.User">
124
- <template>
125
- <div class="card">
126
- <f-text path="Name"></f-text>
127
- <f-field path="Email"></f-field>
128
- </div>
129
- </template>
130
- </f-each>
186
+ <f-field path="user.name"></f-field>
187
+ <f-field path="user.role" type="select">
188
+ <option value="admin">Admin</option>
189
+ <option value="user">User</option>
190
+ </f-field>
131
191
  ```
132
192
 
133
- Paths inside templates are relative to each item.
134
-
135
- ### `<f-when>` — Conditionals
193
+ ### `<f-when>` Conditional
136
194
 
137
195
  ```html
138
- <f-when test="User.LoggedIn == 'true'">
139
- <span>Welcome!</span>
196
+ <f-when test="user.active == 'true'">
197
+ <span>Active!</span>
140
198
  </f-when>
141
199
 
142
- <f-when test="Cart.Total > 100">
200
+ <f-when test="cart.total > 100">
143
201
  <span>Free shipping!</span>
144
202
  </f-when>
145
203
  ```
@@ -147,225 +205,179 @@ Paths inside templates are relative to each item.
147
205
  ### `<f-match>` — Switch
148
206
 
149
207
  ```html
150
- <f-match test="User.Role">
208
+ <f-match test="user.role">
151
209
  <f-case value="admin">Admin Panel</f-case>
152
210
  <f-case value="user">Dashboard</f-case>
153
211
  <f-else>Please log in</f-else>
154
212
  </f-match>
155
213
  ```
156
214
 
157
- ---
158
-
159
- ## Store Discovery
160
-
161
- Components don't need to be nested inside `<f-state>`. Flarp searches siblings and ancestors:
215
+ ### `<f-each>` — Iteration
162
216
 
163
217
  ```html
164
- <div class="app">
165
- <f-state id="app">
166
- <User><n>Alice</n></User>
167
- </f-state>
168
-
169
- <main>
170
- <!-- Finds sibling f-state automatically -->
171
- <f-text path="User.Name"></f-text>
172
- </main>
173
- </div>
218
+ <f-each path="users.user">
219
+ <template>
220
+ <div class="user">
221
+ <f-text path="name"></f-text>
222
+ </div>
223
+ </template>
224
+ </f-each>
174
225
  ```
175
226
 
176
- Or reference explicitly:
227
+ ---
177
228
 
178
- ```html
179
- <f-text store="app" path="User.Name"></f-text>
180
- ```
229
+ ## JavaScript API
181
230
 
182
- ---
231
+ ```javascript
232
+ const store = document.querySelector('f-store');
183
233
 
184
- ## Event Sourcing
234
+ // Wait for ready
235
+ store.state.when('ready', () => {
185
236
 
186
- ```js
187
- // Register actions
188
- store.on('addUser', ({ name, role }) => {
189
- store.add('Users', `<User><n>${name}</n><Role>${role}</Role></User>`);
190
- });
237
+ // Get reactive node
238
+ const name = store.at('user.name');
191
239
 
192
- // Dispatch
193
- store.emit('addUser', { name: 'Bob', role: 'Developer' });
194
- ```
240
+ // Read/write
241
+ console.log(name.value);
242
+ name.value = 'Bob';
195
243
 
196
- ---
244
+ // Subscribe to changes
245
+ name.subscribe(v => console.log('Name changed:', v));
197
246
 
198
- ## Per-Node Reactivity
247
+ // Query multiple
248
+ const users = store.query('users.user');
199
249
 
200
- Every XML node is independently:
201
- - **Reactive** — Has its own Signal
202
- - **Identifiable** — `uuid` attribute (stable identity)
203
- - **Versioned** — `rev` for conflict detection & resolution
204
- - **Serializable** — `node.serialize()`
250
+ // Add node
251
+ store.add('users', '<user><n>New User</n></user>');
205
252
 
206
- ```js
207
- const node = store.at('user.name');
253
+ // Remove node
254
+ store.remove('users.user[0]');
208
255
 
209
- node.uuid; // "a1b2c3..." (stable identity)
210
- node.rev; // "5-f94bc318..." (revision)
211
- node.revNumber; // 5
212
- node.revId; // "f94bc318..." (write identifier)
256
+ // Serialize
257
+ const xml = store.serialize();
213
258
 
214
- node.serialize(); // '<name uuid="..." rev="5-...">Alice</name>'
259
+ // Manual save
260
+ store.save();
261
+
262
+ // Apply external update
263
+ store.applyRemote('<n uuid="..." rev="5-abc">Value</n>');
264
+ });
215
265
 
216
- // Subscribe to just this node
217
- node.subscribe(value => console.log(value));
266
+ // Event handlers
267
+ store.onChange(xml => console.log('Changed'));
268
+ store.onConflict(info => console.log('Conflict:', info));
218
269
  ```
219
270
 
220
271
  ---
221
272
 
222
- ## Revision Format (CouchDB-style)
223
-
224
- Flarp uses CouchDB-style revision tracking: `{number}-{uuid}`
273
+ ## Architecture
225
274
 
226
275
  ```
227
- rev="3-ee30f92d-a785-4603-b0b8-b681b8707e39"
228
- │ └─ Revision UUID (fresh on each write)
229
- └─ Revision number (increments)
276
+ flarp/
277
+ ├── src/
278
+ │ ├── core/ # Reactive primitives
279
+ │ │ ├── Signal.js # Synchronous reactive value
280
+ │ │ └── State.js # Named states (ready, synced, etc.)
281
+ │ │
282
+ │ ├── xml/ # XML utilities
283
+ │ │ ├── Node.js # Reactive node wrapper
284
+ │ │ ├── Path.js # Path resolution
285
+ │ │ └── Tree.js # Tree management
286
+ │ │
287
+ │ ├── sync/ # Persistence & sync
288
+ │ │ ├── Sync.js # Cross-tab sync with changes feed
289
+ │ │ ├── Store.js # FStore component
290
+ │ │ ├── Persist.js # Storage adapters
291
+ │ │ └── Channel.js # BroadcastChannel wrapper
292
+ │ │
293
+ │ ├── dom/ # DOM utilities
294
+ │ │ └── find.js # Store discovery
295
+ │ │
296
+ │ ├── components/ # Web Components
297
+ │ │ ├── FText.js
298
+ │ │ ├── FField.js
299
+ │ │ ├── FWhen.js
300
+ │ │ ├── FEach.js
301
+ │ │ └── FBind.js
302
+ │ │
303
+ │ └── index.js # Main exports
304
+
305
+ ├── index.html # Demo
306
+ ├── multiuser.html # Multi-tab sync demo
307
+ └── README.md
230
308
  ```
231
309
 
232
- **Why two parts?**
233
-
234
- 1. **Revision number** — Quick comparison (higher wins)
235
- 2. **Revision UUID** — Conflict detection & tie-breaking
310
+ ---
236
311
 
237
- **Conflict scenario:**
238
- ```
239
- User A saves: rev="3-ee30f92d..."
240
- User B saves: rev="3-56b1d51a..."
241
- ↑ Same number = CONFLICT detected
312
+ ## Multi-User Demo
242
313
 
243
- Tie-breaker: Sort UUIDs alphabetically, first wins
244
- "56b1d51a" < "ee30f92d" → User B wins
245
- ```
314
+ Open `multiuser.html` in multiple browser tabs to see sync in action:
246
315
 
247
- This ensures:
248
- - No data loss (both writes saved with unique UUIDs)
249
- - Consistent winner selection (all nodes agree independently)
250
- - Eventual consistency across distributed systems
316
+ 1. Each tab gets a unique user ID
317
+ 2. Changes sync instantly across all tabs
318
+ 3. Conflicts are detected and resolved automatically
319
+ 4. Enable "auto-write" to stress test
251
320
 
252
- ```js
253
- // Check for conflict
254
- if (node.conflictsWith(remoteRev)) {
255
- console.log('Conflict detected!');
256
- }
321
+ ```bash
322
+ # Start a local server
323
+ npm run dev
257
324
 
258
- // Merge with automatic conflict resolution
259
- const result = node.merge(remoteXml);
260
- // { applied: true, conflict: true, winner: 'remote' }
325
+ # Open multiple tabs to:
326
+ http://localhost:8080/multiuser.html
261
327
  ```
262
328
 
263
329
  ---
264
330
 
265
- ## Persistence
331
+ ## Saving & Persistence
266
332
 
267
- State persists to localStorage automatically:
333
+ ### Automatic
268
334
 
269
335
  ```html
270
- <f-state key="myapp" autosave="500">
336
+ <f-store key="myapp" autosave="500">
337
+ <!-- Saves 500ms after last change -->
338
+ </f-store>
271
339
  ```
272
340
 
273
- Features:
274
- - **Crash recovery** — Reload resumes state
275
- - **Cross-tab sync** — BroadcastChannel
276
- - **Conflict resolution** — Higher `rev` wins, `uuid` tiebreaker
277
-
278
- ---
279
-
280
- ## Modular Architecture
281
-
282
- Flarp is built from composable pieces:
341
+ ### Manual
283
342
 
284
- ```js
285
- // Just the signal primitive
286
- import { Signal } from 'flarp/core';
287
-
288
- // Just XML utilities
289
- import { Node, Path } from 'flarp/xml';
290
-
291
- // Just persistence
292
- import { Persist } from 'flarp/sync';
343
+ ```javascript
344
+ store.save(); // Save now
345
+ store.clear(); // Clear stored state
293
346
  ```
294
347
 
295
- ---
296
-
297
- ## Custom Components
298
-
299
- ```js
300
- import { findStore } from 'flarp';
301
-
302
- class UserCard extends HTMLElement {
303
- connectedCallback() {
304
- const store = findStore(this);
305
-
306
- store.state.when('ready', () => {
307
- const name = store.at('User.Name');
308
-
309
- name.subscribe(v => {
310
- this.innerHTML = `<h2>${v}</h2>`;
311
- });
312
- });
313
- }
314
- }
348
+ ### Emergency Save
315
349
 
316
- customElements.define('user-card', UserCard);
317
- ```
318
-
319
- ---
350
+ Flarp automatically saves on `beforeunload` (browser close/refresh).
320
351
 
321
- ## Philosophy
352
+ ### Storage Location
322
353
 
323
- 1. **XML is data** Human-readable, self-describing, AI-friendly
324
- 2. **Signals are sync** — No scheduler, no batching, just updates
325
- 3. **DOM is truth** — MutationObserver, not virtual DOM
326
- 4. **State ≠ UI** — Keep them separate, connect with paths
327
- 5. **Persist by default** — State survives refresh
354
+ Data is stored in localStorage under `flarp-data:{key}`.
328
355
 
329
356
  ---
330
357
 
331
358
  ## Tag Naming
332
359
 
333
- **Important:** The browser's HTML parser lowercases all tag names. When you write:
360
+ ⚠️ **Use lowercase tags!** The DOM lowercases all tag names, so `<User>` becomes `<user>`. For clarity, always write lowercase:
334
361
 
335
362
  ```html
336
- <f-state>
337
- <User><Name>Alice</Name></User>
338
- </f-state>
339
- ```
340
-
341
- The DOM actually contains:
342
-
343
- ```html
344
- <f-state>
345
- <user><name>Alice</name></user>
346
- </f-state>
347
- ```
348
-
349
- **Flarp handles this automatically** — path matching is case-insensitive. Both `user.name` and `User.Name` will work. However, we recommend using lowercase tags for clarity:
350
-
351
- ```html
352
- <f-state>
353
- <user>
354
- <name>Alice</name>
355
- <email>alice@example.com</email>
356
- </user>
357
- </f-state>
363
+ <!-- Good -->
364
+ <f-store>
365
+ <user><n>Alice</n></user>
366
+ </f-store>
367
+
368
+ <!-- Works but confusing -->
369
+ <f-store>
370
+ <User><n>Alice</n></User>
371
+ </f-store>
358
372
  ```
359
373
 
360
374
  ---
361
375
 
362
376
  ## Browser Support
363
377
 
364
- Modern browsers with ES modules:
365
- - Chrome 61+
366
- - Firefox 60+
367
- - Safari 11+
368
- - Edge 79+
378
+ - Modern browsers with BroadcastChannel (Chrome, Firefox, Safari, Edge)
379
+ - Falls back to localStorage events for older browsers
380
+ - Requires ES modules support
369
381
 
370
382
  ---
371
383
 
@@ -373,14 +385,10 @@ Modern browsers with ES modules:
373
385
 
374
386
  flarp (from Jargon File)
375
387
 
376
- /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.
388
+ /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.
377
389
 
378
390
  ---
379
391
 
380
392
  ## License
381
393
 
382
394
  MIT
383
-
384
- ---
385
-
386
- *Built for developers who believe state management can be simple.*
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flarp",
3
- "version": "2.0.1",
4
- "description": "DOM-native XML state management for Web Components",
3
+ "version": "2.5.1",
4
+ "description": "DOM-native XML state management with multi-master sync",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "module": "src/index.js",
@@ -2,7 +2,9 @@
2
2
  * Flarp Web Components
3
3
  */
4
4
 
5
- export { default as FStore } from './FStore.js';
5
+ // FStore is in sync module for better organization
6
+ export { FStore } from '../sync/index.js';
7
+
6
8
  export { default as FText } from './FText.js';
7
9
  export { default as FField } from './FField.js';
8
10
  export { default as FBind } from './FBind.js';