@jsswift/core 1.0.41
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/dist/cms.js +5280 -0
- package/dist/cms.min.js +1 -0
- package/dist/min-cms.js +1 -0
- package/package.json +27 -0
- package/src/00-bootstrap.js +393 -0
- package/src/09-overlay-shared.js +162 -0
- package/src/10-overlay.js +213 -0
- package/src/15-dom-props.js +211 -0
- package/src/16-renderer-shared.js +308 -0
- package/src/17-renderer-children.js +134 -0
- package/src/18-renderer-args.js +73 -0
- package/src/20-renderer.js +575 -0
- package/src/30-rod-core.js +270 -0
- package/src/31-rod-model.js +157 -0
- package/src/32-lifecycle-helpers.js +84 -0
- package/src/32-lifecycle.js +131 -0
- package/src/33-debug-perf.js +176 -0
- package/src/34-rod-devtools.js +43 -0
- package/src/39-store-shared.js +60 -0
- package/src/40-store-core.js +203 -0
- package/src/41-store-extensions.js +194 -0
- package/src/42-plugins.js +163 -0
- package/src/42a-auth-shared.js +148 -0
- package/src/43-auth.js +171 -0
- package/src/43a-http-shared.js +234 -0
- package/src/44-http.js +157 -0
- package/src/45-route-hooks.js +55 -0
- package/src/49-router-shared.js +99 -0
- package/src/49a-ui-meta-shared.js +116 -0
- package/src/50-ui-meta.js +189 -0
- package/src/51-can.js +20 -0
- package/src/52-router.js +399 -0
- package/src/53-footer.js +142 -0
- package/src/README.md +58 -0
- package/src/modules.json +31 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// Debug utilities
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift.debug = (() => {
|
|
5
|
+
const counters = {
|
|
6
|
+
rodFlushes: 0,
|
|
7
|
+
rodNotifies: 0,
|
|
8
|
+
rodBinds: 0,
|
|
9
|
+
rodBindNull: 0,
|
|
10
|
+
rodBindDup: 0,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function enabled() {
|
|
14
|
+
return !!JSswift.config.debug;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function log(...a) { if (enabled()) console.log("[JSswift]", ...a); }
|
|
18
|
+
function warn(...a) { if (enabled()) console.warn("[JSswift]", ...a); }
|
|
19
|
+
function error(...a) { console.error("[JSswift]", ...a); } // error sempre
|
|
20
|
+
|
|
21
|
+
function inc(name, by = 1) {
|
|
22
|
+
if (!counters[name]) counters[name] = 0;
|
|
23
|
+
counters[name] += by;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function stats() {
|
|
27
|
+
// snapshot immutabile
|
|
28
|
+
return { ...counters };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function reset() {
|
|
32
|
+
for (const k of Object.keys(counters)) counters[k] = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { enabled, log, warn, error, inc, stats, reset };
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
// ===============================
|
|
39
|
+
// Performance DevTools
|
|
40
|
+
// ===============================
|
|
41
|
+
JSswift.perf = (() => {
|
|
42
|
+
let enabled = false;
|
|
43
|
+
|
|
44
|
+
const counters = {
|
|
45
|
+
effectsRun: 0,
|
|
46
|
+
effectsSlow: 0,
|
|
47
|
+
rodNotifies: 0,
|
|
48
|
+
rodFlushes: 0,
|
|
49
|
+
domWrites: 0,
|
|
50
|
+
httpRequests: 0
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const timeline = []; // {t,type,ms,meta}
|
|
54
|
+
const slow = []; // {ms, meta}
|
|
55
|
+
|
|
56
|
+
const cfg = {
|
|
57
|
+
timelineMax: 200,
|
|
58
|
+
slowEffectMs: 8, // soglia "lento"
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function now() {
|
|
62
|
+
return (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pushEvent(type, ms = 0, meta = null) {
|
|
66
|
+
if (!enabled) return;
|
|
67
|
+
timeline.push({ t: Date.now(), type, ms, meta });
|
|
68
|
+
if (timeline.length > cfg.timelineMax) timeline.shift();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function inc(name, by = 1) {
|
|
72
|
+
if (!enabled) return;
|
|
73
|
+
counters[name] = (counters[name] || 0) + by;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function trackSlow(ms, meta) {
|
|
77
|
+
if (!enabled) return;
|
|
78
|
+
slow.push({ t: Date.now(), ms, meta });
|
|
79
|
+
if (slow.length > 100) slow.shift();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- patch reactive.effect ---
|
|
83
|
+
let _origEffect = null;
|
|
84
|
+
|
|
85
|
+
function patchEffect() {
|
|
86
|
+
if (_origEffect) return;
|
|
87
|
+
_origEffect = JSswift.reactive.effect;
|
|
88
|
+
|
|
89
|
+
JSswift.reactive.effect = function (fn, meta) {
|
|
90
|
+
// meta opzionale (string/object) per identificare
|
|
91
|
+
const wrapped = (...args) => {
|
|
92
|
+
const t0 = now();
|
|
93
|
+
inc("effectsRun");
|
|
94
|
+
try { return fn(...args); }
|
|
95
|
+
finally {
|
|
96
|
+
const dt = now() - t0;
|
|
97
|
+
if (dt >= cfg.slowEffectMs) {
|
|
98
|
+
inc("effectsSlow");
|
|
99
|
+
trackSlow(dt, meta || fn.name || "effect");
|
|
100
|
+
pushEvent("effect:slow", dt, meta || fn.name || null);
|
|
101
|
+
} else {
|
|
102
|
+
pushEvent("effect", dt, meta || fn.name || null);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return _origEffect(wrapped);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function unpatchEffect() {
|
|
111
|
+
if (_origEffect) {
|
|
112
|
+
JSswift.reactive.effect = _origEffect;
|
|
113
|
+
_origEffect = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Public API
|
|
118
|
+
function enable(on = true) {
|
|
119
|
+
enabled = !!on;
|
|
120
|
+
if (enabled) patchEffect();
|
|
121
|
+
else unpatchEffect();
|
|
122
|
+
return enabled;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function reset() {
|
|
126
|
+
for (const k of Object.keys(counters)) counters[k] = 0;
|
|
127
|
+
timeline.length = 0;
|
|
128
|
+
slow.length = 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function stats() {
|
|
132
|
+
return {
|
|
133
|
+
enabled,
|
|
134
|
+
...counters,
|
|
135
|
+
slowEffectMs: cfg.slowEffectMs,
|
|
136
|
+
timelineSize: timeline.length
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getTimeline() {
|
|
141
|
+
return timeline.slice();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function slowEffects(minMs = cfg.slowEffectMs) {
|
|
145
|
+
return slow.filter(x => x.ms >= minMs).slice().sort((a, b) => b.ms - a.ms);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// helper per misurare manualmente
|
|
149
|
+
function time(label, fn) {
|
|
150
|
+
const t0 = now();
|
|
151
|
+
try { return fn(); }
|
|
152
|
+
finally {
|
|
153
|
+
const dt = now() - t0;
|
|
154
|
+
pushEvent("time:" + label, dt, label);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// hook utili da chiamare in rod/http
|
|
159
|
+
function mark(type, meta) { pushEvent(type, 0, meta); }
|
|
160
|
+
function tick(type, ms, meta) { pushEvent(type, ms, meta); }
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
enable,
|
|
164
|
+
reset,
|
|
165
|
+
stats,
|
|
166
|
+
timeline: getTimeline,
|
|
167
|
+
slowEffects,
|
|
168
|
+
time,
|
|
169
|
+
mark,
|
|
170
|
+
tick,
|
|
171
|
+
inc,
|
|
172
|
+
cfg
|
|
173
|
+
};
|
|
174
|
+
})();
|
|
175
|
+
|
|
176
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// Rod DevTools micro
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift.rod = JSswift.rod || {};
|
|
5
|
+
|
|
6
|
+
JSswift.rod.inspect = function (r, label = "rod") {
|
|
7
|
+
if (!r || r.type !== "rod") {
|
|
8
|
+
console.warn("[JSswift.rod.inspect] non è un rod:", r);
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const bindings = typeof r.bindings === "function" ? r.bindings() : [];
|
|
13
|
+
const info = {
|
|
14
|
+
label,
|
|
15
|
+
value: r.value,
|
|
16
|
+
bindingsCount: bindings.length,
|
|
17
|
+
bindings: bindings.map(b => ({
|
|
18
|
+
key: b.key,
|
|
19
|
+
el: b.el?.nodeType === 3 ? "#text" : b.el?.tagName,
|
|
20
|
+
id: b.el?.id || null,
|
|
21
|
+
className: b.el?.className || null,
|
|
22
|
+
isConnected: b.el?.nodeType === 3 ? !!b.el.parentNode : (b.el?.isConnected ?? null)
|
|
23
|
+
})),
|
|
24
|
+
actionsCount: Array.isArray(r._actions) ? r._actions.length : null,
|
|
25
|
+
disposed: !!r._disposed
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (JSswift.config.debug) {
|
|
29
|
+
console.groupCollapsed(`[JSswift.rod.inspect] ${label}`);
|
|
30
|
+
console.log(info);
|
|
31
|
+
console.groupEnd();
|
|
32
|
+
} else {
|
|
33
|
+
console.log(info);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return info;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
JSswift.rod.inspectAll = function () {
|
|
40
|
+
const all = JSswift.rod._all ? Array.from(JSswift.rod._all) : [];
|
|
41
|
+
all.forEach((r, i) => JSswift.rod.inspect(r, `rod#${i + 1}`));
|
|
42
|
+
return all.length;
|
|
43
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// Store shared helpers
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift._storeShared = (() => {
|
|
5
|
+
function scopeId(scope) {
|
|
6
|
+
return `${scope.storage}::${scope.prefix}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function createScopeTools(config, knownScopes) {
|
|
10
|
+
function getScope(opts = {}) {
|
|
11
|
+
const scope = {
|
|
12
|
+
storage: opts.storage ?? config.storage,
|
|
13
|
+
prefix: opts.prefix ?? config.prefix
|
|
14
|
+
};
|
|
15
|
+
knownScopes.set(scopeId(scope), scope);
|
|
16
|
+
return scope;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getStorage(scope = null) {
|
|
20
|
+
const mode = typeof scope === "string" ? scope : (scope?.storage ?? config.storage);
|
|
21
|
+
return mode === "session" ? window.sessionStorage : window.localStorage;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fullKey(key, scope = null) {
|
|
25
|
+
const resolved = scope || getScope();
|
|
26
|
+
return resolved.prefix + key;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function channelKey(key, scope = null) {
|
|
30
|
+
const resolved = scope || getScope();
|
|
31
|
+
return `${scopeId(resolved)}::${key}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { getScope, getStorage, fullKey, channelKey };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function safeParse(str) {
|
|
38
|
+
try { return JSON.parse(str); } catch { return undefined; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeStringify(value) {
|
|
42
|
+
try { return JSON.stringify(value); } catch { return undefined; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clearScopedMapEntries(scope, ...maps) {
|
|
46
|
+
const prefix = `${scope.storage}::${scope.prefix}::`;
|
|
47
|
+
for (const map of maps) {
|
|
48
|
+
for (const id of Array.from(map.keys())) {
|
|
49
|
+
if (id.startsWith(prefix)) map.delete(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
createScopeTools,
|
|
56
|
+
safeParse,
|
|
57
|
+
safeStringify,
|
|
58
|
+
clearScopedMapEntries
|
|
59
|
+
};
|
|
60
|
+
})();
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// JSswift.store v1 (persisted reactive state)
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift.store = (() => {
|
|
5
|
+
const config = {
|
|
6
|
+
prefix: "JSswift:",
|
|
7
|
+
storage: "local", // "local" | "session"
|
|
8
|
+
syncTabs: true,
|
|
9
|
+
writeDelay: 0, // ms (0 = microtask)
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const mem = new Map(); // channelKey -> value (runtime cache)
|
|
13
|
+
const watchers = new Map(); // channelKey -> Set(fn)
|
|
14
|
+
const pendingWrites = new Map();// channelKey -> { key, scope }
|
|
15
|
+
const knownScopes = new Map(); // scopeId -> { storage, prefix }
|
|
16
|
+
let writeQueued = false;
|
|
17
|
+
const {
|
|
18
|
+
createScopeTools,
|
|
19
|
+
safeParse,
|
|
20
|
+
safeStringify,
|
|
21
|
+
clearScopedMapEntries
|
|
22
|
+
} = JSswift._storeShared;
|
|
23
|
+
const {
|
|
24
|
+
getScope,
|
|
25
|
+
getStorage,
|
|
26
|
+
fullKey,
|
|
27
|
+
channelKey
|
|
28
|
+
} = createScopeTools(config, knownScopes);
|
|
29
|
+
|
|
30
|
+
function emit(key, value, scope = null) {
|
|
31
|
+
const resolved = scope || getScope();
|
|
32
|
+
const set = watchers.get(channelKey(key, resolved));
|
|
33
|
+
if (!set) return;
|
|
34
|
+
for (const fn of set) {
|
|
35
|
+
try {
|
|
36
|
+
if (JSswift.reactive?.untracked) {
|
|
37
|
+
JSswift.reactive.untracked(() => fn(value));
|
|
38
|
+
} else {
|
|
39
|
+
fn(value);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) { console.error("[store] watcher error:", e); }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function flushWrites() {
|
|
46
|
+
writeQueued = false;
|
|
47
|
+
for (const [id, pending] of Array.from(pendingWrites.entries())) {
|
|
48
|
+
pendingWrites.delete(id);
|
|
49
|
+
const v = mem.get(id);
|
|
50
|
+
const s = safeStringify(v);
|
|
51
|
+
if (s === undefined) continue;
|
|
52
|
+
const st = getStorage(pending.scope);
|
|
53
|
+
try { st.setItem(fullKey(pending.key, pending.scope), s); }
|
|
54
|
+
catch (e) { console.error("[store] write error:", e); }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function scheduleWrite(key, scope = null) {
|
|
59
|
+
const resolved = scope || getScope();
|
|
60
|
+
pendingWrites.set(channelKey(key, resolved), { key, scope: resolved });
|
|
61
|
+
if (writeQueued) return;
|
|
62
|
+
writeQueued = true;
|
|
63
|
+
|
|
64
|
+
if (config.writeDelay > 0) {
|
|
65
|
+
setTimeout(flushWrites, config.writeDelay);
|
|
66
|
+
} else {
|
|
67
|
+
queueMicrotask(flushWrites);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readFromStorage(key, scope = null) {
|
|
72
|
+
const resolved = scope || getScope();
|
|
73
|
+
const st = getStorage(resolved);
|
|
74
|
+
const raw = st.getItem(fullKey(key, resolved));
|
|
75
|
+
if (raw == null) return undefined;
|
|
76
|
+
return safeParse(raw);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function get(key, fallback, scope = null) {
|
|
80
|
+
const resolved = scope || getScope();
|
|
81
|
+
const id = channelKey(key, resolved);
|
|
82
|
+
if (mem.has(id)) return mem.get(id);
|
|
83
|
+
const v = readFromStorage(key, resolved);
|
|
84
|
+
if (v !== undefined) {
|
|
85
|
+
mem.set(id, v);
|
|
86
|
+
return v;
|
|
87
|
+
}
|
|
88
|
+
return fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function set(key, value, scope = null) {
|
|
92
|
+
const resolved = scope || getScope();
|
|
93
|
+
mem.set(channelKey(key, resolved), value);
|
|
94
|
+
scheduleWrite(key, resolved);
|
|
95
|
+
emit(key, value, resolved);
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function remove(key, scope = null) {
|
|
100
|
+
const resolved = scope || getScope();
|
|
101
|
+
const id = channelKey(key, resolved);
|
|
102
|
+
mem.delete(id);
|
|
103
|
+
pendingWrites.delete(id);
|
|
104
|
+
try { getStorage(resolved).removeItem(fullKey(key, resolved)); } catch { }
|
|
105
|
+
emit(key, undefined, resolved);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clear(opts = {}) {
|
|
109
|
+
const scope = getScope(opts);
|
|
110
|
+
// rimuove solo chiavi col prefix nel relativo storage
|
|
111
|
+
const st = getStorage(scope);
|
|
112
|
+
const keys = [];
|
|
113
|
+
for (let i = 0; i < st.length; i++) {
|
|
114
|
+
const k = st.key(i);
|
|
115
|
+
if (k && k.startsWith(scope.prefix)) keys.push(k);
|
|
116
|
+
}
|
|
117
|
+
for (const k of keys) st.removeItem(k);
|
|
118
|
+
|
|
119
|
+
clearScopedMapEntries(scope, mem, pendingWrites, watchers);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function watch(key, fn, scope = null) {
|
|
123
|
+
if (typeof fn !== "function") return () => { };
|
|
124
|
+
const resolved = scope || getScope();
|
|
125
|
+
const id = channelKey(key, resolved);
|
|
126
|
+
if (!watchers.has(id)) watchers.set(id, new Set());
|
|
127
|
+
watchers.get(id).add(fn);
|
|
128
|
+
return () => watchers.get(id)?.delete(fn);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Signal persistente (core feature)
|
|
132
|
+
function signal(key, initial, opts = {}) {
|
|
133
|
+
const scope = getScope({
|
|
134
|
+
storage: opts.storage,
|
|
135
|
+
prefix: opts.prefix
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const hydrated = get(key, initial, scope);
|
|
139
|
+
const [getSig, setSig, disposeSignal] = JSswift.reactive.signal(hydrated);
|
|
140
|
+
|
|
141
|
+
// quando cambia signal -> store, deve restare nello scope corretto
|
|
142
|
+
const stopEffect = JSswift.reactive.effect(() => {
|
|
143
|
+
const v = getSig();
|
|
144
|
+
set(key, v, scope);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// quando cambia store (cross-tab o set manuale nello stesso scope) -> signal
|
|
148
|
+
const unwatch = watch(key, (v) => {
|
|
149
|
+
// evita loop inutili
|
|
150
|
+
if (getSig() !== v) setSig(v);
|
|
151
|
+
}, scope);
|
|
152
|
+
|
|
153
|
+
// cleanup helper
|
|
154
|
+
const dispose = () => {
|
|
155
|
+
try { stopEffect?.(); } catch { }
|
|
156
|
+
try { unwatch?.(); } catch { }
|
|
157
|
+
try { disposeSignal?.(); } catch { }
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return [getSig, setSig, dispose];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getScope();
|
|
164
|
+
|
|
165
|
+
// cross-tab sync
|
|
166
|
+
if (config.syncTabs) {
|
|
167
|
+
window.addEventListener("storage", (e) => {
|
|
168
|
+
if (!e.key || !e.storageArea) return;
|
|
169
|
+
|
|
170
|
+
for (const scope of knownScopes.values()) {
|
|
171
|
+
if (getStorage(scope) !== e.storageArea) continue;
|
|
172
|
+
if (!e.key.startsWith(scope.prefix)) continue;
|
|
173
|
+
|
|
174
|
+
const key = e.key.slice(scope.prefix.length);
|
|
175
|
+
const v = e.newValue == null ? undefined : safeParse(e.newValue);
|
|
176
|
+
const id = channelKey(key, scope);
|
|
177
|
+
|
|
178
|
+
// aggiorna cache + watchers (senza forzare write)
|
|
179
|
+
if (v === undefined) mem.delete(id);
|
|
180
|
+
else mem.set(id, v);
|
|
181
|
+
|
|
182
|
+
emit(key, v, scope);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function configure(next = {}) {
|
|
188
|
+
Object.assign(config, next);
|
|
189
|
+
getScope();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function stats() {
|
|
193
|
+
return {
|
|
194
|
+
prefix: config.prefix,
|
|
195
|
+
storage: config.storage,
|
|
196
|
+
syncTabs: config.syncTabs,
|
|
197
|
+
cacheSize: mem.size,
|
|
198
|
+
scopeCount: knownScopes.size,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { configure, stats, get, set, remove, clear, watch, signal };
|
|
203
|
+
})();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// 1) store.computed (derivato, non persistito)
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift.store.computed = function (fn) {
|
|
5
|
+
if (typeof fn !== "function") throw new Error("store.computed: fn must be a function");
|
|
6
|
+
return JSswift.reactive.computed(fn);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// ===============================
|
|
11
|
+
// 2) store.migrate (versioning + migrazioni)
|
|
12
|
+
// ===============================
|
|
13
|
+
// targetVersion: numero intero (es. 3)
|
|
14
|
+
// steps: object { 0: fn, 1: fn, 2: fn } dove la chiave è la versione "from"
|
|
15
|
+
// step[v] porta da v -> v+1
|
|
16
|
+
JSswift.store.migrate = function (targetVersion, steps, opts = {}) {
|
|
17
|
+
const versionKey = opts.versionKey ?? "__version";
|
|
18
|
+
const from = Number(JSswift.store.get(versionKey, 0) ?? 0);
|
|
19
|
+
const to = Number(targetVersion ?? 0);
|
|
20
|
+
|
|
21
|
+
if (!Number.isFinite(to) || to < 0) throw new Error("store.migrate: targetVersion non valido");
|
|
22
|
+
if (from >= to) return { from, to, applied: 0 };
|
|
23
|
+
|
|
24
|
+
let applied = 0;
|
|
25
|
+
|
|
26
|
+
for (let v = from; v < to; v++) {
|
|
27
|
+
const step = steps?.[v];
|
|
28
|
+
if (typeof step === "function") {
|
|
29
|
+
try {
|
|
30
|
+
step(JSswift.store);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error("[store.migrate] step error at version", v, e);
|
|
33
|
+
// puoi scegliere se interrompere qui: io continuo ma è discutibile.
|
|
34
|
+
// return { from, to, applied, error: e };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
applied++;
|
|
38
|
+
// salva versione raggiunta (più sicuro step-by-step)
|
|
39
|
+
JSswift.store.set(versionKey, v + 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { from, to, applied };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// ===============================
|
|
47
|
+
// 3) store.model (form persistenti + rodModel)
|
|
48
|
+
// ===============================
|
|
49
|
+
// elOrList: Element | NodeList | Array<Element> (radio group ecc.)
|
|
50
|
+
// key: string per storage
|
|
51
|
+
// initial: valore iniziale se non presente
|
|
52
|
+
// opts: { storage, prefix, event, parse, format }
|
|
53
|
+
JSswift.store.model = function (elOrList, key, initial, opts = {}) {
|
|
54
|
+
if (typeof key !== "string" || !key) throw new Error("store.model: key deve essere una stringa non vuota");
|
|
55
|
+
|
|
56
|
+
// signal persistente
|
|
57
|
+
const [get, set, disposeSignal] = JSswift.store.signal(key, initial, {
|
|
58
|
+
storage: opts.storage,
|
|
59
|
+
prefix: opts.prefix,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// bridge signal <-> rod
|
|
63
|
+
const r = JSswift.rodFromSignal(get, set);
|
|
64
|
+
|
|
65
|
+
// two-way con elementi form (checkbox/radio/select/text/number)
|
|
66
|
+
const unbindModel = JSswift.rodModel(elOrList, r, {
|
|
67
|
+
event: opts.event,
|
|
68
|
+
parse: opts.parse,
|
|
69
|
+
format: opts.format
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// cleanup totale (utile con component dispose)
|
|
73
|
+
return () => {
|
|
74
|
+
try { unbindModel?.(); } catch { }
|
|
75
|
+
try { disposeSignal?.(); } catch { }
|
|
76
|
+
try { r.dispose?.(); } catch { }
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ===============================
|
|
81
|
+
// store.bind (DX alias super corto)
|
|
82
|
+
// ===============================
|
|
83
|
+
// selectorOrEl: "#id" | Element | NodeList | Array<Element>
|
|
84
|
+
// key: string storage key
|
|
85
|
+
// initial: valore iniziale
|
|
86
|
+
// opts: { storage, prefix, event, parse, format }
|
|
87
|
+
JSswift.store.bind = function (selectorOrEl, key, initial, opts = {}) {
|
|
88
|
+
let target = selectorOrEl;
|
|
89
|
+
|
|
90
|
+
// risolve selector string
|
|
91
|
+
if (typeof selectorOrEl === "string") {
|
|
92
|
+
// se è un radio group: input[name=...]
|
|
93
|
+
if (selectorOrEl.includes("[") || selectorOrEl.includes(" ")) {
|
|
94
|
+
target = document.querySelectorAll(selectorOrEl);
|
|
95
|
+
} else {
|
|
96
|
+
target = JSswift.dom.q(selectorOrEl);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!target) {
|
|
101
|
+
if (JSswift.config?.debug) {
|
|
102
|
+
console.warn("[store.bind] target non trovato:", selectorOrEl);
|
|
103
|
+
}
|
|
104
|
+
return () => { };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return JSswift.store.model(target, key, initial, opts);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ===============================
|
|
111
|
+
// store.bindAll (form -> store)
|
|
112
|
+
// ===============================
|
|
113
|
+
JSswift.store.bindAll = function (formEl, schema = {}, opts = {}) {
|
|
114
|
+
if (typeof formEl === "string") formEl = JSswift.dom.q(formEl);
|
|
115
|
+
if (!formEl || !formEl.querySelectorAll) {
|
|
116
|
+
console.warn("[store.bindAll] form non valido:", formEl);
|
|
117
|
+
return () => { };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const disposers = [];
|
|
121
|
+
|
|
122
|
+
for (const [name, initial] of Object.entries(schema)) {
|
|
123
|
+
const els = formEl.querySelectorAll(`[name="${name}"]`);
|
|
124
|
+
if (!els.length) continue;
|
|
125
|
+
|
|
126
|
+
const unbind = JSswift.store.model(
|
|
127
|
+
els.length === 1 ? els[0] : els,
|
|
128
|
+
name,
|
|
129
|
+
initial,
|
|
130
|
+
opts
|
|
131
|
+
);
|
|
132
|
+
disposers.push(unbind);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return () => disposers.forEach(fn => fn());
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ===============================
|
|
139
|
+
// store.inspect (DevTools)
|
|
140
|
+
// ===============================
|
|
141
|
+
JSswift.store.inspect = function () {
|
|
142
|
+
const info = {
|
|
143
|
+
...JSswift.store.stats(),
|
|
144
|
+
keys: Array.from(
|
|
145
|
+
(function () {
|
|
146
|
+
const st = JSswift.store.stats().storage === "session"
|
|
147
|
+
? sessionStorage
|
|
148
|
+
: localStorage;
|
|
149
|
+
const out = [];
|
|
150
|
+
for (let i = 0; i < st.length; i++) {
|
|
151
|
+
const k = st.key(i);
|
|
152
|
+
if (k && k.startsWith(JSswift.store.stats().prefix)) {
|
|
153
|
+
out.push(k.replace(JSswift.store.stats().prefix, ""));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
})()
|
|
158
|
+
)
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
console.groupCollapsed("[JSswift.store.inspect]");
|
|
162
|
+
console.table(info.keys);
|
|
163
|
+
console.log(info);
|
|
164
|
+
console.groupEnd();
|
|
165
|
+
|
|
166
|
+
return info;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ===============================
|
|
170
|
+
// store.autoForm (autosave + restore)
|
|
171
|
+
// ===============================
|
|
172
|
+
JSswift.store.autoForm = function (formEl, schema = {}, opts = {}) {
|
|
173
|
+
const unbind = JSswift.store.bindAll(formEl, schema, opts);
|
|
174
|
+
|
|
175
|
+
if (JSswift.config?.debug) {
|
|
176
|
+
console.log("[store.autoForm] autosave attivo:", schema);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return unbind;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ===============================
|
|
183
|
+
// useStore (hook-style, component-aware)
|
|
184
|
+
// ===============================
|
|
185
|
+
JSswift.useStore = function (key, initial, ctx, opts = {}) {
|
|
186
|
+
const [get, set, dispose] = JSswift.store.signal(key, initial, opts);
|
|
187
|
+
|
|
188
|
+
// se siamo dentro un component, cleanup automatico
|
|
189
|
+
if (ctx && typeof ctx.onDispose === "function" && typeof dispose === "function") {
|
|
190
|
+
ctx.onDispose(dispose);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return [get, set];
|
|
194
|
+
};
|