@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.
@@ -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
+ };