flarp 1.0.0
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/LICENSE +21 -0
- package/README.md +332 -0
- package/package.json +48 -0
- package/src/components/FBind.js +76 -0
- package/src/components/FEach.js +194 -0
- package/src/components/FField.js +153 -0
- package/src/components/FStore.js +332 -0
- package/src/components/FText.js +73 -0
- package/src/components/FWhen.js +253 -0
- package/src/components/index.js +10 -0
- package/src/core/Signal.js +220 -0
- package/src/core/State.js +168 -0
- package/src/core/index.js +6 -0
- package/src/dom/find.js +128 -0
- package/src/dom/index.js +5 -0
- package/src/index.js +63 -0
- package/src/sync/Channel.js +113 -0
- package/src/sync/Persist.js +216 -0
- package/src/sync/index.js +6 -0
- package/src/xml/Node.js +396 -0
- package/src/xml/Path.js +176 -0
- package/src/xml/Tree.js +279 -0
- package/src/xml/index.js +7 -0
package/src/dom/find.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM utilities for Flarp
|
|
3
|
+
*
|
|
4
|
+
* Intelligent store discovery that doesn't require nesting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find nearest store by searching up the DOM tree
|
|
9
|
+
*
|
|
10
|
+
* @param {Element} element - Starting element
|
|
11
|
+
* @param {string} selector - Store selector (default: 'f-state')
|
|
12
|
+
* @returns {Element|null}
|
|
13
|
+
*
|
|
14
|
+
* Search order:
|
|
15
|
+
* 1. Explicit store="id" attribute
|
|
16
|
+
* 2. Scoped store from f-each
|
|
17
|
+
* 3. Nearest f-state in same container hierarchy
|
|
18
|
+
* 4. Document-level fallback by ID proximity
|
|
19
|
+
*/
|
|
20
|
+
export function findStore(element, selector = 'f-state') {
|
|
21
|
+
// 1. Check explicit reference
|
|
22
|
+
const ref = element.getAttribute('store');
|
|
23
|
+
if (ref) {
|
|
24
|
+
const explicit = document.getElementById(ref);
|
|
25
|
+
if (explicit) return explicit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Check scoped store (from f-each)
|
|
29
|
+
if (element._store) {
|
|
30
|
+
return element._store;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Walk up and search within each ancestor's subtree
|
|
34
|
+
let current = element.parentElement;
|
|
35
|
+
|
|
36
|
+
while (current) {
|
|
37
|
+
// Search for f-state within this ancestor (but not within nested components)
|
|
38
|
+
const store = current.querySelector(selector);
|
|
39
|
+
if (store) {
|
|
40
|
+
return store;
|
|
41
|
+
}
|
|
42
|
+
current = current.parentElement;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 4. Fallback - shouldn't normally reach here if structure is correct
|
|
46
|
+
return document.querySelector(selector);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find all stores relevant to an element
|
|
51
|
+
* @param {Element} element
|
|
52
|
+
* @param {string} selector
|
|
53
|
+
* @returns {Element[]}
|
|
54
|
+
*/
|
|
55
|
+
export function findAllStores(element, selector = 'f-state') {
|
|
56
|
+
const stores = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
let current = element.parentElement;
|
|
59
|
+
|
|
60
|
+
while (current) {
|
|
61
|
+
for (const store of current.querySelectorAll(selector)) {
|
|
62
|
+
if (!seen.has(store)) {
|
|
63
|
+
seen.add(store);
|
|
64
|
+
stores.push(store);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
current = current.parentElement;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return stores;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wait for store to be ready
|
|
75
|
+
* @param {Element} store
|
|
76
|
+
* @returns {Promise}
|
|
77
|
+
*/
|
|
78
|
+
export function whenReady(store) {
|
|
79
|
+
if (store.state?.is('ready')) {
|
|
80
|
+
return Promise.resolve(store);
|
|
81
|
+
}
|
|
82
|
+
return store.state?.until('ready').then(() => store)
|
|
83
|
+
|| Promise.resolve(store);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a scoped store proxy for iteration contexts
|
|
88
|
+
* @param {Object} parentStore - Parent store reference
|
|
89
|
+
* @param {Element} scopeElement - XML element for this iteration
|
|
90
|
+
* @returns {Object} Scoped store proxy
|
|
91
|
+
*/
|
|
92
|
+
export function scopedStore(parentStore, scopeElement) {
|
|
93
|
+
return {
|
|
94
|
+
at(path) {
|
|
95
|
+
if (path === '.' || path === '') {
|
|
96
|
+
const { Node } = parentStore.constructor;
|
|
97
|
+
return Node ? Node.wrap(scopeElement) : null;
|
|
98
|
+
}
|
|
99
|
+
// Resolve relative to scope element
|
|
100
|
+
const { Path, Node, AttrNode } = parentStore.constructor;
|
|
101
|
+
const resolved = Path.resolve(scopeElement, path);
|
|
102
|
+
if (!resolved) return null;
|
|
103
|
+
|
|
104
|
+
if (resolved.attr) {
|
|
105
|
+
return AttrNode.wrap(resolved.element, resolved.attr);
|
|
106
|
+
}
|
|
107
|
+
return Node.wrap(resolved.element);
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
query(path) {
|
|
111
|
+
const { Path, Node } = parentStore.constructor;
|
|
112
|
+
return Path.resolveAll(scopeElement, path).map(el => Node.wrap(el));
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Delegate other methods to parent
|
|
116
|
+
add: parentStore.add?.bind(parentStore),
|
|
117
|
+
remove: parentStore.remove?.bind(parentStore),
|
|
118
|
+
emit: parentStore.emit?.bind(parentStore),
|
|
119
|
+
on: parentStore.on?.bind(parentStore),
|
|
120
|
+
state: parentStore.state,
|
|
121
|
+
|
|
122
|
+
// Mark as scoped
|
|
123
|
+
_scoped: true,
|
|
124
|
+
_element: scopeElement
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default { findStore, findAllStores, whenReady, scopedStore };
|
package/src/dom/index.js
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flarp - DOM-native XML State Management
|
|
3
|
+
*
|
|
4
|
+
* XML is state. Signals are reactive. The DOM is the runtime.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```html
|
|
8
|
+
* <f-state key="myapp" autosave="500">
|
|
9
|
+
* <User>
|
|
10
|
+
* <n>Alice</n>
|
|
11
|
+
* <Role>Developer</Role>
|
|
12
|
+
* </User>
|
|
13
|
+
* </f-state>
|
|
14
|
+
*
|
|
15
|
+
* <main>
|
|
16
|
+
* <f-text path="User.Name"></f-text>
|
|
17
|
+
* <f-field path="User.Role"></f-field>
|
|
18
|
+
* </main>
|
|
19
|
+
*
|
|
20
|
+
* <script type="module">
|
|
21
|
+
* import 'flarp';
|
|
22
|
+
*
|
|
23
|
+
* const store = document.querySelector('f-state');
|
|
24
|
+
*
|
|
25
|
+
* store.state.when('ready', () => {
|
|
26
|
+
* const name = store.at('User.Name');
|
|
27
|
+
* name.subscribe(v => console.log('Name:', v));
|
|
28
|
+
* });
|
|
29
|
+
* </script>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
// Core reactive primitives
|
|
34
|
+
export { default as Signal } from './core/Signal.js';
|
|
35
|
+
export { default as State } from './core/State.js';
|
|
36
|
+
|
|
37
|
+
// XML utilities
|
|
38
|
+
export { default as Node, AttrNode } from './xml/Node.js';
|
|
39
|
+
export { default as Tree } from './xml/Tree.js';
|
|
40
|
+
export * as Path from './xml/Path.js';
|
|
41
|
+
|
|
42
|
+
// Sync utilities
|
|
43
|
+
export * as Persist from './sync/Persist.js';
|
|
44
|
+
export { default as Channel, Protocol } from './sync/Channel.js';
|
|
45
|
+
|
|
46
|
+
// DOM utilities
|
|
47
|
+
export { findStore, findAllStores, whenReady, scopedStore } from './dom/find.js';
|
|
48
|
+
|
|
49
|
+
// Web Components
|
|
50
|
+
export {
|
|
51
|
+
FStore,
|
|
52
|
+
FText,
|
|
53
|
+
FField,
|
|
54
|
+
FBind,
|
|
55
|
+
FEach,
|
|
56
|
+
FWhen,
|
|
57
|
+
FMatch,
|
|
58
|
+
FCase,
|
|
59
|
+
FElse
|
|
60
|
+
} from './components/index.js';
|
|
61
|
+
|
|
62
|
+
// Version
|
|
63
|
+
export const VERSION = '2.0.0';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel - Cross-tab synchronization via BroadcastChannel
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const channel = new Channel('myapp');
|
|
6
|
+
*
|
|
7
|
+
* channel.onMessage(data => {
|
|
8
|
+
* console.log('Received from other tab:', data);
|
|
9
|
+
* });
|
|
10
|
+
*
|
|
11
|
+
* channel.send({ type: 'update', xml: '...' });
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export default class Channel {
|
|
15
|
+
#channel;
|
|
16
|
+
#key;
|
|
17
|
+
#handlers = new Set();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} key - Channel identifier
|
|
21
|
+
*/
|
|
22
|
+
constructor(key) {
|
|
23
|
+
this.#key = key;
|
|
24
|
+
|
|
25
|
+
if (typeof BroadcastChannel !== 'undefined') {
|
|
26
|
+
this.#channel = new BroadcastChannel(`flarp:${key}`);
|
|
27
|
+
this.#channel.onmessage = (e) => {
|
|
28
|
+
for (const handler of this.#handlers) {
|
|
29
|
+
try {
|
|
30
|
+
handler(e.data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.error('Channel handler error:', err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Send data to other tabs
|
|
41
|
+
* @param {*} data - Serializable data
|
|
42
|
+
*/
|
|
43
|
+
send(data) {
|
|
44
|
+
this.#channel?.postMessage(data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register message handler
|
|
49
|
+
* @param {Function} fn - Handler function
|
|
50
|
+
* @returns {Function} Unsubscribe
|
|
51
|
+
*/
|
|
52
|
+
onMessage(fn) {
|
|
53
|
+
this.#handlers.add(fn);
|
|
54
|
+
return () => this.#handlers.delete(fn);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Close channel
|
|
59
|
+
*/
|
|
60
|
+
close() {
|
|
61
|
+
this.#channel?.close();
|
|
62
|
+
this.#handlers.clear();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if BroadcastChannel is supported
|
|
67
|
+
*/
|
|
68
|
+
static get supported() {
|
|
69
|
+
return typeof BroadcastChannel !== 'undefined';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sync protocol messages
|
|
75
|
+
*/
|
|
76
|
+
export const Protocol = {
|
|
77
|
+
UPDATE: 'update',
|
|
78
|
+
REQUEST: 'request',
|
|
79
|
+
RESPONSE: 'response',
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create update message
|
|
83
|
+
*/
|
|
84
|
+
update(xml, nodeId = null) {
|
|
85
|
+
return {
|
|
86
|
+
type: this.UPDATE,
|
|
87
|
+
xml,
|
|
88
|
+
nodeId,
|
|
89
|
+
timestamp: Date.now()
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create state request
|
|
95
|
+
*/
|
|
96
|
+
request() {
|
|
97
|
+
return {
|
|
98
|
+
type: this.REQUEST,
|
|
99
|
+
timestamp: Date.now()
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create state response
|
|
105
|
+
*/
|
|
106
|
+
response(xml) {
|
|
107
|
+
return {
|
|
108
|
+
type: this.RESPONSE,
|
|
109
|
+
xml,
|
|
110
|
+
timestamp: Date.now()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist - Storage adapters for XML state
|
|
3
|
+
*
|
|
4
|
+
* Supports localStorage, sessionStorage, and IndexedDB.
|
|
5
|
+
* All adapters share the same async interface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PREFIX = 'flarp:';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* localStorage adapter
|
|
12
|
+
*/
|
|
13
|
+
export const local = {
|
|
14
|
+
name: 'localStorage',
|
|
15
|
+
|
|
16
|
+
async get(key) {
|
|
17
|
+
try {
|
|
18
|
+
return localStorage.getItem(PREFIX + key);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.warn('localStorage get failed:', e);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async set(key, value) {
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(PREFIX + key, value);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn('localStorage set failed:', e);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async remove(key) {
|
|
36
|
+
try {
|
|
37
|
+
localStorage.removeItem(PREFIX + key);
|
|
38
|
+
return true;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async keys() {
|
|
45
|
+
const result = [];
|
|
46
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
47
|
+
const key = localStorage.key(i);
|
|
48
|
+
if (key?.startsWith(PREFIX)) {
|
|
49
|
+
result.push(key.slice(PREFIX.length));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* sessionStorage adapter (per-tab)
|
|
58
|
+
*/
|
|
59
|
+
export const session = {
|
|
60
|
+
name: 'sessionStorage',
|
|
61
|
+
|
|
62
|
+
async get(key) {
|
|
63
|
+
try {
|
|
64
|
+
return sessionStorage.getItem(PREFIX + key);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async set(key, value) {
|
|
71
|
+
try {
|
|
72
|
+
sessionStorage.setItem(PREFIX + key, value);
|
|
73
|
+
return true;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async remove(key) {
|
|
80
|
+
try {
|
|
81
|
+
sessionStorage.removeItem(PREFIX + key);
|
|
82
|
+
return true;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
async keys() {
|
|
89
|
+
const result = [];
|
|
90
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
91
|
+
const key = sessionStorage.key(i);
|
|
92
|
+
if (key?.startsWith(PREFIX)) {
|
|
93
|
+
result.push(key.slice(PREFIX.length));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* IndexedDB adapter (for large state)
|
|
102
|
+
*/
|
|
103
|
+
export const indexed = {
|
|
104
|
+
name: 'indexedDB',
|
|
105
|
+
_db: null,
|
|
106
|
+
_dbName: 'flarp',
|
|
107
|
+
_storeName: 'state',
|
|
108
|
+
|
|
109
|
+
async _open() {
|
|
110
|
+
if (this._db) return this._db;
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const request = indexedDB.open(this._dbName, 1);
|
|
114
|
+
|
|
115
|
+
request.onerror = () => reject(request.error);
|
|
116
|
+
|
|
117
|
+
request.onupgradeneeded = (e) => {
|
|
118
|
+
const db = e.target.result;
|
|
119
|
+
if (!db.objectStoreNames.contains(this._storeName)) {
|
|
120
|
+
db.createObjectStore(this._storeName);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
request.onsuccess = () => {
|
|
125
|
+
this._db = request.result;
|
|
126
|
+
resolve(this._db);
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async get(key) {
|
|
132
|
+
try {
|
|
133
|
+
const db = await this._open();
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const tx = db.transaction(this._storeName, 'readonly');
|
|
136
|
+
const store = tx.objectStore(this._storeName);
|
|
137
|
+
const request = store.get(key);
|
|
138
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
139
|
+
request.onerror = () => reject(request.error);
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async set(key, value) {
|
|
147
|
+
try {
|
|
148
|
+
const db = await this._open();
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const tx = db.transaction(this._storeName, 'readwrite');
|
|
151
|
+
const store = tx.objectStore(this._storeName);
|
|
152
|
+
const request = store.put(value, key);
|
|
153
|
+
request.onsuccess = () => resolve(true);
|
|
154
|
+
request.onerror = () => reject(request.error);
|
|
155
|
+
});
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async remove(key) {
|
|
162
|
+
try {
|
|
163
|
+
const db = await this._open();
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
const tx = db.transaction(this._storeName, 'readwrite');
|
|
166
|
+
const store = tx.objectStore(this._storeName);
|
|
167
|
+
const request = store.delete(key);
|
|
168
|
+
request.onsuccess = () => resolve(true);
|
|
169
|
+
request.onerror = () => reject(request.error);
|
|
170
|
+
});
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async keys() {
|
|
177
|
+
try {
|
|
178
|
+
const db = await this._open();
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const tx = db.transaction(this._storeName, 'readonly');
|
|
181
|
+
const store = tx.objectStore(this._storeName);
|
|
182
|
+
const request = store.getAllKeys();
|
|
183
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
184
|
+
request.onerror = () => reject(request.error);
|
|
185
|
+
});
|
|
186
|
+
} catch (e) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get best available adapter
|
|
194
|
+
*/
|
|
195
|
+
export function auto() {
|
|
196
|
+
try {
|
|
197
|
+
localStorage.setItem('__test__', '1');
|
|
198
|
+
localStorage.removeItem('__test__');
|
|
199
|
+
return local;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return indexed;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Debounce utility
|
|
207
|
+
*/
|
|
208
|
+
export function debounce(fn, ms) {
|
|
209
|
+
let timer = null;
|
|
210
|
+
return (...args) => {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default { local, session, indexed, auto, debounce };
|