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.
@@ -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 };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * DOM - DOM utilities for Flarp
3
+ */
4
+
5
+ export { findStore, findAllStores, whenReady, scopedStore } from './find.js';
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 };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sync - Persistence and cross-tab synchronization
3
+ */
4
+
5
+ export * as Persist from './Persist.js';
6
+ export { default as Channel, Protocol } from './Channel.js';