@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,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
+ })();