@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,163 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// Plugin system (JSswift.usePlugin)
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift._plugins = new Set();
|
|
5
|
+
|
|
6
|
+
JSswift.usePlugin = function (plugin, options) {
|
|
7
|
+
if (!plugin) return;
|
|
8
|
+
|
|
9
|
+
// evita doppia installazione
|
|
10
|
+
if (JSswift._plugins.has(plugin)) {
|
|
11
|
+
if (JSswift.config?.debug) {
|
|
12
|
+
console.warn("[JSswift.usePlugin] plugin già installato:", plugin.name || plugin);
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// funzione-plugin
|
|
18
|
+
if (typeof plugin === "function") {
|
|
19
|
+
plugin(JSswift, options);
|
|
20
|
+
JSswift._plugins.add(plugin);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// oggetto-plugin { install(app, opts) }
|
|
25
|
+
if (plugin && typeof plugin.install === "function") {
|
|
26
|
+
plugin.install(JSswift, options);
|
|
27
|
+
JSswift._plugins.add(plugin);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.warn("[JSswift.usePlugin] plugin non valido:", plugin);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
//-- RESTA come esempio --
|
|
35
|
+
JSswift.plugins = JSswift.plugins || {};
|
|
36
|
+
JSswift.plugins.debug = {
|
|
37
|
+
install(app) {
|
|
38
|
+
app.config.debug = true;
|
|
39
|
+
console.log("[JSswift] Debug mode ON");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ===============================
|
|
44
|
+
// Plugin Forms (validation + UX)
|
|
45
|
+
// ===============================
|
|
46
|
+
JSswift.plugins.forms = {
|
|
47
|
+
install(app) {
|
|
48
|
+
const forms = {};
|
|
49
|
+
|
|
50
|
+
app.forms = {
|
|
51
|
+
validate(formEl, rules = {}, opts = {}) {
|
|
52
|
+
if (typeof formEl === "string") formEl = app.dom.q(formEl);
|
|
53
|
+
if (!formEl || !formEl.querySelectorAll) {
|
|
54
|
+
console.warn("[forms.validate] form non valido:", formEl);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const options = {
|
|
59
|
+
mode: opts.mode || "input", // input | change | blur
|
|
60
|
+
showMessages: opts.showMessages ?? true,
|
|
61
|
+
submitGuard: opts.submitGuard ?? true,
|
|
62
|
+
errorClass: opts.errorClass || "is-invalid",
|
|
63
|
+
validClass: opts.validClass || "is-valid",
|
|
64
|
+
messageClass: opts.messageClass || "form-error"
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const fieldState = {}; // name -> { getErr, setErr }
|
|
68
|
+
|
|
69
|
+
// setup fields
|
|
70
|
+
for (const [name, rule] of Object.entries(rules)) {
|
|
71
|
+
const els = formEl.querySelectorAll(`[name="${name}"]`);
|
|
72
|
+
if (!els.length) continue;
|
|
73
|
+
|
|
74
|
+
const [getErr, setErr] = app.reactive.signal(null);
|
|
75
|
+
fieldState[name] = { getErr, setErr, els };
|
|
76
|
+
|
|
77
|
+
// message node (uno per campo)
|
|
78
|
+
let msgNode = null;
|
|
79
|
+
if (options.showMessages) {
|
|
80
|
+
msgNode = document.createElement("div");
|
|
81
|
+
msgNode.className = options.messageClass;
|
|
82
|
+
els[els.length - 1].after(msgNode);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const validateValue = () => {
|
|
86
|
+
let value;
|
|
87
|
+
const el = els[0];
|
|
88
|
+
|
|
89
|
+
if (el.type === "checkbox") value = el.checked;
|
|
90
|
+
else if (el.type === "radio") {
|
|
91
|
+
const c = Array.from(els).find(r => r.checked);
|
|
92
|
+
value = c ? c.value : null;
|
|
93
|
+
} else value = el.value;
|
|
94
|
+
|
|
95
|
+
let res = true;
|
|
96
|
+
try {
|
|
97
|
+
res = rule(value);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
res = e.message || "Errore di validazione";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const error = res === true ? null : (typeof res === "string" ? res : "Valore non valido");
|
|
103
|
+
setErr(error);
|
|
104
|
+
return !error;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// bind events
|
|
108
|
+
for (const el of els) {
|
|
109
|
+
el.addEventListener(options.mode, validateValue);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// reactive UI
|
|
113
|
+
app.reactive.effect(() => {
|
|
114
|
+
const err = getErr();
|
|
115
|
+
for (const el of els) {
|
|
116
|
+
el.classList.toggle(options.errorClass, !!err);
|
|
117
|
+
el.classList.toggle(options.validClass, !err);
|
|
118
|
+
}
|
|
119
|
+
if (msgNode) {
|
|
120
|
+
msgNode.textContent = err || "";
|
|
121
|
+
msgNode.style.display = err ? "block" : "none";
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// initial check
|
|
126
|
+
validateValue();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// computed form validity
|
|
130
|
+
const isValid = app.store.computed(() => {
|
|
131
|
+
return Object.values(fieldState).every(f => !f.getErr());
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// submit guard
|
|
135
|
+
const onSubmit = (e) => {
|
|
136
|
+
let ok = true;
|
|
137
|
+
for (const f of Object.values(fieldState)) {
|
|
138
|
+
const el = f.els[0];
|
|
139
|
+
if (el) el.dispatchEvent(new Event(options.mode, { bubbles: true }));
|
|
140
|
+
if (f.getErr()) ok = false;
|
|
141
|
+
}
|
|
142
|
+
if (!ok && options.submitGuard) {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (options.submitGuard) {
|
|
149
|
+
formEl.addEventListener("submit", onSubmit);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const dispose = () => {
|
|
153
|
+
if (options.submitGuard) {
|
|
154
|
+
formEl.removeEventListener("submit", onSubmit);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
forms[formEl] = { isValid, dispose };
|
|
159
|
+
return { isValid, dispose };
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// Auth shared helpers
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift._authShared = (() => {
|
|
5
|
+
function createPermissionApi(getUser) {
|
|
6
|
+
const roles = () => getUser()?.roles || [];
|
|
7
|
+
const perms = () => getUser()?.permissions || [];
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
roles,
|
|
11
|
+
perms,
|
|
12
|
+
hasRole: (role) => roles().includes(role),
|
|
13
|
+
can: (permission) => perms().includes(permission),
|
|
14
|
+
canAny: (list) => list.some((item) => roles().includes(item) || perms().includes(item)),
|
|
15
|
+
canAll: (list) => list.every((item) => roles().includes(item) || perms().includes(item))
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function matchesProtectedPath(path, protectedList) {
|
|
20
|
+
return protectedList.some((entry) =>
|
|
21
|
+
typeof entry === "string"
|
|
22
|
+
? path.startsWith(entry)
|
|
23
|
+
: entry instanceof RegExp
|
|
24
|
+
? entry.test(path)
|
|
25
|
+
: false
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function attachDevTools(app, auth) {
|
|
30
|
+
if (!app || !auth) return;
|
|
31
|
+
|
|
32
|
+
let tracing = false;
|
|
33
|
+
|
|
34
|
+
function safeNow() {
|
|
35
|
+
return Date.now();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeJWT(token) {
|
|
39
|
+
if (!token || typeof token !== "string") return null;
|
|
40
|
+
const parts = token.split(".");
|
|
41
|
+
if (parts.length < 2) return null;
|
|
42
|
+
try {
|
|
43
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
44
|
+
const json = decodeURIComponent(
|
|
45
|
+
atob(b64).split("").map((char) => "%" + char.charCodeAt(0).toString(16).padStart(2, "0")).join("")
|
|
46
|
+
);
|
|
47
|
+
return JSON.parse(json);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getAuthState() {
|
|
54
|
+
if (typeof auth._getState === "function") {
|
|
55
|
+
const state = auth._getState();
|
|
56
|
+
return state && typeof state === "object"
|
|
57
|
+
? state
|
|
58
|
+
: { user: auth.user?.() ?? null };
|
|
59
|
+
}
|
|
60
|
+
return { user: auth.user?.() ?? null };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function status() {
|
|
64
|
+
const state = getAuthState() || {};
|
|
65
|
+
const user = state.user ?? auth.user?.();
|
|
66
|
+
const name = user?.name || user?.email || user?.id || "anon";
|
|
67
|
+
const ok = !!auth.isAuth?.();
|
|
68
|
+
const expiresAt = state.expiresAt || null;
|
|
69
|
+
const left = expiresAt ? expiresAt - safeNow() : null;
|
|
70
|
+
return ok
|
|
71
|
+
? `AUTH ✅ user=${name}${expiresAt ? ` expiresIn=${Math.round(left / 1000)}s` : ""}`
|
|
72
|
+
: `AUTH ❌ user=${name}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function inspect(label = "auth") {
|
|
76
|
+
const state = getAuthState() || {};
|
|
77
|
+
const user = state.user ?? auth.user?.();
|
|
78
|
+
const roles = user?.roles || [];
|
|
79
|
+
const perms = user?.permissions || [];
|
|
80
|
+
const accessToken = state.accessToken || null;
|
|
81
|
+
const refreshToken = state.refreshToken || null;
|
|
82
|
+
const jwt = accessToken ? decodeJWT(accessToken) : null;
|
|
83
|
+
const expiresAt = state.expiresAt || (jwt?.exp ? jwt.exp * 1000 : null);
|
|
84
|
+
const now = safeNow();
|
|
85
|
+
const expiresInMs = expiresAt ? (expiresAt - now) : null;
|
|
86
|
+
|
|
87
|
+
const info = {
|
|
88
|
+
label,
|
|
89
|
+
isAuth: !!auth.isAuth?.(),
|
|
90
|
+
user,
|
|
91
|
+
roles,
|
|
92
|
+
permissions: perms,
|
|
93
|
+
has: {
|
|
94
|
+
role: (role) => !!auth.hasRole?.(role),
|
|
95
|
+
can: (permission) => !!auth.can?.(permission)
|
|
96
|
+
},
|
|
97
|
+
token: {
|
|
98
|
+
hasAccess: !!accessToken,
|
|
99
|
+
hasRefresh: !!refreshToken,
|
|
100
|
+
expiresAt,
|
|
101
|
+
expiresInSec: expiresInMs == null ? null : Math.round(expiresInMs / 1000),
|
|
102
|
+
decodedJWT: jwt
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
console.groupCollapsed(`[JSswift.auth.inspect] ${label}`);
|
|
107
|
+
console.log("status:", status());
|
|
108
|
+
console.log(info);
|
|
109
|
+
if (expiresInMs != null) {
|
|
110
|
+
if (expiresInMs <= 0) console.warn("⚠️ Access token scaduto.");
|
|
111
|
+
else if (expiresInMs < 60_000) console.warn("⚠️ Access token in scadenza (<60s).");
|
|
112
|
+
}
|
|
113
|
+
console.groupEnd();
|
|
114
|
+
|
|
115
|
+
return info;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function trace(on = true) {
|
|
119
|
+
tracing = !!on;
|
|
120
|
+
console.log("[JSswift.auth.trace]", tracing ? "ON" : "OFF");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof auth.fetch === "function" && !auth._fetchWrapped) {
|
|
124
|
+
const originalFetch = auth.fetch.bind(auth);
|
|
125
|
+
auth.fetch = async (...args) => {
|
|
126
|
+
const res = await originalFetch(...args);
|
|
127
|
+
if (tracing) {
|
|
128
|
+
try {
|
|
129
|
+
console.log("[auth.fetch]", res.status, args[0]);
|
|
130
|
+
} catch { }
|
|
131
|
+
}
|
|
132
|
+
return res;
|
|
133
|
+
};
|
|
134
|
+
auth._fetchWrapped = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
auth.decodeJWT = decodeJWT;
|
|
138
|
+
auth.status = status;
|
|
139
|
+
auth.inspect = inspect;
|
|
140
|
+
auth.trace = trace;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
createPermissionApi,
|
|
145
|
+
matchesProtectedPath,
|
|
146
|
+
attachDevTools
|
|
147
|
+
};
|
|
148
|
+
})();
|
package/src/43-auth.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// Auth Plugin (store + router guard)
|
|
3
|
+
// Auth Plugin + Roles / Permissions
|
|
4
|
+
// Auth Plugin (async + refresh token)
|
|
5
|
+
// ===============================
|
|
6
|
+
JSswift.plugins.auth = {
|
|
7
|
+
install(app, opts = {}) {
|
|
8
|
+
const {
|
|
9
|
+
createPermissionApi,
|
|
10
|
+
matchesProtectedPath,
|
|
11
|
+
attachDevTools
|
|
12
|
+
} = JSswift._authShared;
|
|
13
|
+
const options = {
|
|
14
|
+
key: opts.key || "auth",
|
|
15
|
+
loginRoute: opts.loginRoute || "/login",
|
|
16
|
+
protected: opts.protected || [],
|
|
17
|
+
autoRedirect: opts.autoRedirect ?? true,
|
|
18
|
+
|
|
19
|
+
// async config
|
|
20
|
+
endpoints: opts.endpoints || {
|
|
21
|
+
login: "/api/login",
|
|
22
|
+
refresh: "/api/refresh",
|
|
23
|
+
logout: "/api/logout"
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
fetchOptions: opts.fetchOptions || {},
|
|
27
|
+
tokenHeader: opts.tokenHeader || "Authorization",
|
|
28
|
+
tokenPrefix: opts.tokenPrefix || "Bearer ",
|
|
29
|
+
|
|
30
|
+
refreshMargin: opts.refreshMargin || 30_000 // ms prima della scadenza
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------- STORE ----------
|
|
34
|
+
const [getAuth, setAuth] = app.store.signal(options.key, null);
|
|
35
|
+
|
|
36
|
+
const getUser = () => getAuth()?.user || null;
|
|
37
|
+
const isAuth = () => !!getAuth()?.accessToken;
|
|
38
|
+
|
|
39
|
+
// ---------- ROLES / PERMS ----------
|
|
40
|
+
const {
|
|
41
|
+
hasRole,
|
|
42
|
+
can,
|
|
43
|
+
canAny,
|
|
44
|
+
canAll
|
|
45
|
+
} = createPermissionApi(getUser);
|
|
46
|
+
|
|
47
|
+
// ---------- INTERNAL STATE ----------
|
|
48
|
+
let refreshing = false;
|
|
49
|
+
let refreshQueue = [];
|
|
50
|
+
|
|
51
|
+
// ---------- HELPERS ----------
|
|
52
|
+
function needsRefresh() {
|
|
53
|
+
const a = getAuth();
|
|
54
|
+
if (!a?.expiresAt) return false;
|
|
55
|
+
return Date.now() > (a.expiresAt - options.refreshMargin);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function doRefresh() {
|
|
59
|
+
if (refreshing) {
|
|
60
|
+
return new Promise((res, rej) => refreshQueue.push({ res, rej }));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
refreshing = true;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const r = await fetch(options.endpoints.refresh, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({ refreshToken: getAuth()?.refreshToken })
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!r.ok) throw new Error("Refresh failed");
|
|
73
|
+
|
|
74
|
+
const data = await r.json();
|
|
75
|
+
setAuth({ ...getAuth(), ...data });
|
|
76
|
+
|
|
77
|
+
refreshQueue.forEach(q => q.res(true));
|
|
78
|
+
refreshQueue = [];
|
|
79
|
+
return true;
|
|
80
|
+
|
|
81
|
+
} catch (e) {
|
|
82
|
+
refreshQueue.forEach(q => q.rej(e));
|
|
83
|
+
refreshQueue = [];
|
|
84
|
+
setAuth(null);
|
|
85
|
+
if (options.autoRedirect) app.router.navigate(options.loginRoute);
|
|
86
|
+
throw e;
|
|
87
|
+
|
|
88
|
+
} finally {
|
|
89
|
+
refreshing = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------- ASYNC AUTH API ----------
|
|
94
|
+
async function loginAsync(credentials) {
|
|
95
|
+
const r = await fetch(options.endpoints.login, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify(credentials)
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!r.ok) throw new Error("Login failed");
|
|
102
|
+
const data = await r.json();
|
|
103
|
+
setAuth(data);
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function logoutAsync() {
|
|
108
|
+
try {
|
|
109
|
+
await fetch(options.endpoints.logout, { method: "POST" });
|
|
110
|
+
} catch { }
|
|
111
|
+
setAuth(null);
|
|
112
|
+
if (options.autoRedirect) app.router.navigate(options.loginRoute);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------- AUTH FETCH ----------
|
|
116
|
+
async function authFetch(url, opts = {}) {
|
|
117
|
+
if (needsRefresh()) await doRefresh();
|
|
118
|
+
|
|
119
|
+
const a = getAuth();
|
|
120
|
+
const headers = new Headers(opts.headers || {});
|
|
121
|
+
if (a?.accessToken) {
|
|
122
|
+
headers.set(options.tokenHeader, options.tokenPrefix + a.accessToken);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const r = await fetch(url, { ...options.fetchOptions, ...opts, headers });
|
|
126
|
+
|
|
127
|
+
if (r.status === 401 && getAuth()?.refreshToken && !opts._authRetried) {
|
|
128
|
+
await doRefresh();
|
|
129
|
+
return authFetch(url, { ...opts, _authRetried: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return r;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------- ROUTER GUARD ----------
|
|
136
|
+
app.router.beforeEach((ctx) => {
|
|
137
|
+
const path = ctx.path;
|
|
138
|
+
const protectedMatch = matchesProtectedPath(path, options.protected);
|
|
139
|
+
|
|
140
|
+
if (protectedMatch && !isAuth()) {
|
|
141
|
+
return options.loginRoute;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ---------- PUBLIC API ----------
|
|
147
|
+
app.auth = {
|
|
148
|
+
user: getUser,
|
|
149
|
+
isAuth,
|
|
150
|
+
hasRole,
|
|
151
|
+
can,
|
|
152
|
+
canAny,
|
|
153
|
+
canAll,
|
|
154
|
+
loginAsync,
|
|
155
|
+
logoutAsync,
|
|
156
|
+
fetch: authFetch,
|
|
157
|
+
_getState: getAuth
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ---------- HOOK ----------
|
|
161
|
+
app.useAuth = function (ctx) {
|
|
162
|
+
if (ctx && typeof ctx.onDispose === "function") {
|
|
163
|
+
// future-proof
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return app.auth;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
attachDevTools(JSswift, app.auth);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// ===============================
|
|
2
|
+
// HTTP shared helpers
|
|
3
|
+
// ===============================
|
|
4
|
+
JSswift._httpShared = (() => {
|
|
5
|
+
function createReactiveState(JSswift) {
|
|
6
|
+
const reactive = JSswift.reactive;
|
|
7
|
+
const [getInFlight, setInFlight] = reactive.signal(0);
|
|
8
|
+
const [getStatus, setStatus] = reactive.signal("idle");
|
|
9
|
+
const [getLastRequest, setLastRequest] = reactive.signal(null);
|
|
10
|
+
const [getLastResponse, setLastResponse] = reactive.signal(null);
|
|
11
|
+
const [getLastError, setLastError] = reactive.signal(null);
|
|
12
|
+
const [getLastDuration, setLastDuration] = reactive.signal(0);
|
|
13
|
+
const [getLastUpdated, setLastUpdated] = reactive.signal(0);
|
|
14
|
+
let lastStartId = 0;
|
|
15
|
+
|
|
16
|
+
function markStart(req) {
|
|
17
|
+
const id = ++lastStartId;
|
|
18
|
+
const ts = Date.now();
|
|
19
|
+
setInFlight(getInFlight() + 1);
|
|
20
|
+
setStatus("pending");
|
|
21
|
+
setLastError(null);
|
|
22
|
+
setLastResponse(null);
|
|
23
|
+
setLastRequest({
|
|
24
|
+
id,
|
|
25
|
+
url: req.url,
|
|
26
|
+
method: req.method,
|
|
27
|
+
meta: req.meta,
|
|
28
|
+
timeout: req.timeout,
|
|
29
|
+
retry: req.retry,
|
|
30
|
+
startedAt: ts
|
|
31
|
+
});
|
|
32
|
+
setLastUpdated(ts);
|
|
33
|
+
return id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function markEnd(id, res, err, durationMs) {
|
|
37
|
+
const ts = Date.now();
|
|
38
|
+
setInFlight(Math.max(0, getInFlight() - 1));
|
|
39
|
+
if (id === lastStartId) {
|
|
40
|
+
if (err) {
|
|
41
|
+
setStatus("error");
|
|
42
|
+
setLastError(err);
|
|
43
|
+
setLastResponse(null);
|
|
44
|
+
} else if (res) {
|
|
45
|
+
setStatus("success");
|
|
46
|
+
setLastError(null);
|
|
47
|
+
setLastResponse({
|
|
48
|
+
id,
|
|
49
|
+
status: res.status,
|
|
50
|
+
ok: res.ok,
|
|
51
|
+
headers: res.headers,
|
|
52
|
+
url: res.url,
|
|
53
|
+
receivedAt: ts,
|
|
54
|
+
raw: res
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
setStatus("idle");
|
|
58
|
+
setLastError(null);
|
|
59
|
+
setLastResponse(null);
|
|
60
|
+
}
|
|
61
|
+
setLastDuration(Math.max(0, Number(durationMs || 0)));
|
|
62
|
+
}
|
|
63
|
+
setLastUpdated(ts);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const state = {
|
|
67
|
+
inFlight: getInFlight,
|
|
68
|
+
status: getStatus,
|
|
69
|
+
isLoading: JSswift.store?.computed
|
|
70
|
+
? JSswift.store.computed(() => getInFlight() > 0)
|
|
71
|
+
: () => getInFlight() > 0,
|
|
72
|
+
lastRequest: getLastRequest,
|
|
73
|
+
lastResponse: getLastResponse,
|
|
74
|
+
lastError: getLastError,
|
|
75
|
+
lastDuration: getLastDuration,
|
|
76
|
+
lastUpdated: getLastUpdated,
|
|
77
|
+
reset() {
|
|
78
|
+
setInFlight(0);
|
|
79
|
+
setStatus("idle");
|
|
80
|
+
setLastRequest(null);
|
|
81
|
+
setLastResponse(null);
|
|
82
|
+
setLastError(null);
|
|
83
|
+
setLastDuration(0);
|
|
84
|
+
setLastUpdated(Date.now());
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return { state, markStart, markEnd };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function joinURL(base, path) {
|
|
92
|
+
if (!base) return path;
|
|
93
|
+
if (/^https?:\/\//i.test(path)) return path;
|
|
94
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
95
|
+
const normalizedPath = path.startsWith("/") ? path : "/" + path;
|
|
96
|
+
return normalizedBase + normalizedPath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sleep(ms) {
|
|
100
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isRetryable(err, res) {
|
|
104
|
+
if (err) return true;
|
|
105
|
+
if (!res) return false;
|
|
106
|
+
return res.status === 429 || (res.status >= 500 && res.status <= 599);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function makeAbort(timeoutMs, externalSignal) {
|
|
110
|
+
if (!timeoutMs && !externalSignal) return { signal: undefined, cancel: () => { } };
|
|
111
|
+
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const signal = controller.signal;
|
|
114
|
+
|
|
115
|
+
let timeoutId = null;
|
|
116
|
+
if (timeoutMs > 0) {
|
|
117
|
+
timeoutId = setTimeout(() => controller.abort(new Error("timeout")), timeoutMs);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const onAbort = () => controller.abort(externalSignal.reason || new Error("aborted"));
|
|
121
|
+
if (externalSignal) {
|
|
122
|
+
if (externalSignal.aborted) onAbort();
|
|
123
|
+
else externalSignal.addEventListener("abort", onAbort, { once: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const cancel = () => {
|
|
127
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
128
|
+
if (externalSignal) externalSignal.removeEventListener?.("abort", onAbort);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return { signal, cancel };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeRequest(configHTTP, input, init = {}) {
|
|
135
|
+
const url = typeof input === "string" ? joinURL(configHTTP.baseURL, input) : input;
|
|
136
|
+
const method = (init.method || "GET").toUpperCase();
|
|
137
|
+
const headers = new Headers(configHTTP.headers);
|
|
138
|
+
if (init.headers) new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
url,
|
|
142
|
+
method,
|
|
143
|
+
headers,
|
|
144
|
+
body: init.body,
|
|
145
|
+
credentials: init.credentials ?? configHTTP.credentials,
|
|
146
|
+
signal: init.signal,
|
|
147
|
+
timeout: init.timeout ?? configHTTP.timeout,
|
|
148
|
+
retry: init.retry ?? configHTTP.retry,
|
|
149
|
+
meta: init.meta || {}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function runHooks(set, ...args) {
|
|
154
|
+
let current = args[0];
|
|
155
|
+
for (const fn of set) {
|
|
156
|
+
current = (await fn(current, ...args.slice(1))) ?? current;
|
|
157
|
+
}
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function wrapResponse(res, req) {
|
|
162
|
+
return {
|
|
163
|
+
ok: res.ok,
|
|
164
|
+
status: res.status,
|
|
165
|
+
headers: res.headers,
|
|
166
|
+
raw: res,
|
|
167
|
+
req,
|
|
168
|
+
|
|
169
|
+
async json() {
|
|
170
|
+
let data = null;
|
|
171
|
+
let error = null;
|
|
172
|
+
try { data = await res.json(); } catch { data = null; }
|
|
173
|
+
if (!res.ok) error = data ?? { status: res.status };
|
|
174
|
+
return { data, error, ok: res.ok, status: res.status };
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async jsonStrict() {
|
|
178
|
+
const { data, error } = await this.json();
|
|
179
|
+
if (error) {
|
|
180
|
+
const e = new Error(`HTTP error ${res.status} ${req?.method || ""} ${req?.url || ""}`.trim());
|
|
181
|
+
e.status = res.status;
|
|
182
|
+
e.data = data;
|
|
183
|
+
e.req = {
|
|
184
|
+
url: req?.url,
|
|
185
|
+
method: req?.method,
|
|
186
|
+
meta: req?.meta
|
|
187
|
+
};
|
|
188
|
+
throw e;
|
|
189
|
+
}
|
|
190
|
+
return { data, ok: true, status: res.status };
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
async text() {
|
|
194
|
+
let text = "";
|
|
195
|
+
try { text = await res.text(); } catch { }
|
|
196
|
+
return { text, ok: res.ok, status: res.status };
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async textStrict() {
|
|
200
|
+
const out = await this.text();
|
|
201
|
+
if (!out.ok) {
|
|
202
|
+
const e = new Error("HTTP error " + res.status);
|
|
203
|
+
e.status = res.status;
|
|
204
|
+
e.text = out.text;
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function withJSON(request, method, url, body, init = {}) {
|
|
213
|
+
const headers = new Headers(init.headers || {});
|
|
214
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
215
|
+
return request(url, {
|
|
216
|
+
...init,
|
|
217
|
+
method,
|
|
218
|
+
headers,
|
|
219
|
+
body: body == null ? undefined : JSON.stringify(body)
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
createReactiveState,
|
|
225
|
+
joinURL,
|
|
226
|
+
sleep,
|
|
227
|
+
isRetryable,
|
|
228
|
+
makeAbort,
|
|
229
|
+
normalizeRequest,
|
|
230
|
+
runHooks,
|
|
231
|
+
wrapResponse,
|
|
232
|
+
withJSON
|
|
233
|
+
};
|
|
234
|
+
})();
|