@warkypublic/svelix 0.1.34 → 0.1.35

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.
@@ -9,14 +9,14 @@ type GlobalStateStoreApi = {
9
9
  declare const createGlobalStateStore: () => GlobalStateStoreApi;
10
10
  declare const GlobalStateStore: GlobalStateStoreApi;
11
11
  declare const languageStore: Readable<SupportedLanguage>;
12
+ declare const isLoggedInStore: Readable<boolean>;
12
13
  declare function useGlobalStateStore(): Readable<GlobalStateStoreType>;
13
14
  declare function useGlobalStateStore<T>(selector: (state: GlobalStateStoreType) => T): Readable<T>;
14
15
  declare const setApiURL: (url: string) => void;
15
16
  declare const getApiURL: () => string;
16
17
  declare const getAuthToken: () => string;
17
- declare const isLoggedIn: () => boolean;
18
18
  declare const setAuthToken: (token: string) => void;
19
19
  declare const GetGlobalState: () => GlobalStateStoreType;
20
20
  declare const setLanguage: (lang: SupportedLanguage) => void;
21
21
  export type { GlobalStateStoreApi };
22
- export { createGlobalStateStore, getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
22
+ export { createGlobalStateStore, getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedInStore, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
@@ -102,12 +102,29 @@ const hasSessionValidationSignal = (session, user) => {
102
102
  user?.guid ||
103
103
  user?.username);
104
104
  };
105
+ const computeIsLoggedIn = (session) => {
106
+ if (!session.validated || !session.loggedIn || !session.authToken) {
107
+ return false;
108
+ }
109
+ return !isSessionExpired(session);
110
+ };
105
111
  const createGlobalStateStore = () => {
106
112
  const initialState = createInitialState();
107
113
  let isStorageInitialized = false;
108
114
  let initializationPromise = null;
109
115
  let operationLock = Promise.resolve();
110
116
  let hasAutoValidated = false;
117
+ let validationScheduled = false;
118
+ const scheduleValidation = () => {
119
+ if (validationScheduled)
120
+ return;
121
+ validationScheduled = true;
122
+ waitForInitialization()
123
+ .then(() => withOperationLock(() => fetchDataInternal()))
124
+ .finally(() => {
125
+ validationScheduled = false;
126
+ });
127
+ };
111
128
  const store = writable(undefined);
112
129
  const getState = () => get(store);
113
130
  const setState = (partial, replace = false) => {
@@ -126,7 +143,7 @@ const createGlobalStateStore = () => {
126
143
  hasAutoValidated = true;
127
144
  const session = after.session;
128
145
  if (hasSessionCredentials(session) && !session.validated) {
129
- waitForInitialization().then(() => withOperationLock(() => fetchDataInternal()));
146
+ scheduleValidation();
130
147
  }
131
148
  }
132
149
  };
@@ -258,16 +275,7 @@ const createGlobalStateStore = () => {
258
275
  await waitForInitialization();
259
276
  return withOperationLock(() => fetchDataInternal(url));
260
277
  },
261
- isLoggedIn: () => {
262
- const session = getState().session;
263
- if (!session.validated || !session.loggedIn || !session.authToken) {
264
- return false;
265
- }
266
- if (isSessionExpired(session)) {
267
- return false;
268
- }
269
- return true;
270
- },
278
+ isLoggedIn: () => computeIsLoggedIn(getState().session),
271
279
  login: async (authToken, user) => {
272
280
  await waitForInitialization();
273
281
  return withOperationLock(async () => {
@@ -463,9 +471,6 @@ const createGlobalStateStore = () => {
463
471
  ...loadedState.session,
464
472
  connected: true,
465
473
  loading: false,
466
- loggedIn: hasPersistedSession
467
- ? false
468
- : loadedState.session?.loggedIn ?? current.session.loggedIn,
469
474
  validated: hasPersistedSession
470
475
  ? false
471
476
  : loadedState.session?.validated ?? current.session.validated,
@@ -484,6 +489,12 @@ const createGlobalStateStore = () => {
484
489
  if (!isStorageInitialized) {
485
490
  return;
486
491
  }
492
+ if (hasSessionCredentials(state.session) &&
493
+ !state.session.validated &&
494
+ !computeIsLoggedIn(state.session) &&
495
+ typeof state.onFetchSession === "function") {
496
+ scheduleValidation();
497
+ }
487
498
  saveStorage(toPersistedState(state)).catch((e) => {
488
499
  console.error("Error saving storage:", e);
489
500
  });
@@ -499,6 +510,7 @@ const GlobalStateStoreReadable = {
499
510
  subscribe: GlobalStateStore.subscribe,
500
511
  };
501
512
  const languageStore = derived(GlobalStateStoreReadable, (state) => state.user.language ?? "en");
513
+ const isLoggedInStore = derived(GlobalStateStoreReadable, (state) => computeIsLoggedIn(state.session));
502
514
  initTranslation(languageStore);
503
515
  function useGlobalStateStore(selector) {
504
516
  if (!selector) {
@@ -515,9 +527,6 @@ const getApiURL = () => {
515
527
  const getAuthToken = () => {
516
528
  return GlobalStateStore.getState().session.authToken ?? "";
517
529
  };
518
- const isLoggedIn = () => {
519
- return GlobalStateStore.getState().isLoggedIn();
520
- };
521
530
  const setAuthToken = (token) => {
522
531
  GlobalStateStore.getState().setAuthToken(token);
523
532
  };
@@ -527,4 +536,4 @@ const GetGlobalState = () => {
527
536
  const setLanguage = (lang) => {
528
537
  GlobalStateStore.getState().setLanguage(lang);
529
538
  };
530
- export { createGlobalStateStore, getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
539
+ export { createGlobalStateStore, getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedInStore, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
@@ -0,0 +1,209 @@
1
+ # GlobalStateStore
2
+
3
+ Singleton Svelte store for app-wide authenticated state. Persists all slices to `localStorage` and re-validates credentials on every page load.
4
+
5
+ ---
6
+
7
+ ## Setup
8
+
9
+ Wrap your app with `GlobalStateStoreProvider` and wire up the three server callbacks:
10
+
11
+ ```svelte
12
+ <script lang="ts">
13
+ import { GlobalStateStoreProvider } from '@warkypublic/svelix';
14
+
15
+ const onLogin = async (state) => {
16
+ const res = await api.post('/auth/login', { token: state.session.authToken });
17
+ return { session: res.session, user: res.user };
18
+ };
19
+
20
+ const onLogout = async () => {
21
+ await api.post('/auth/logout');
22
+ };
23
+
24
+ const onFetchSession = async (state) => {
25
+ const res = await api.get('/app/bootstrap');
26
+ return { session: res.session, user: res.user, navigation: res.nav, program: res.program };
27
+ };
28
+ </script>
29
+
30
+ <GlobalStateStoreProvider {onLogin} {onLogout} {onFetchSession}>
31
+ {@render children()}
32
+ </GlobalStateStoreProvider>
33
+ ```
34
+
35
+ ### Provider props
36
+
37
+ | Prop | Type | Default | Description |
38
+ |---|---|---|---|
39
+ | `apiURL` | `string` | — | Sets `session.apiURL` on mount |
40
+ | `autoFetch` | `boolean` | `true` | Enables automatic `fetchData` calls |
41
+ | `fetchOnMount` | `boolean` | `true` | Calls `fetchData` once after mount |
42
+ | `onFetchSession` | `fn` | — | Called by `fetchData`; must return state slice or void |
43
+ | `onLogin` | `fn` | — | Called after credentials are set; return confirms the session |
44
+ | `onLogout` | `fn` | — | Called during logout |
45
+ | `program` | `Partial<ProgramState>` | — | Merged into `program` slice on mount |
46
+ | `throttleMs` | `number` | `0` | Minimum ms between `fetchData` calls |
47
+
48
+ ---
49
+
50
+ ## State slices
51
+
52
+ ### `session`
53
+
54
+ | Field | Type | Description |
55
+ |---|---|---|
56
+ | `apiURL` | `string` | Base API URL |
57
+ | `authToken` | `string` | Bearer token |
58
+ | `loggedIn` | `boolean` | Persisted login intent |
59
+ | `validated` | `boolean` | Server has confirmed the session this load |
60
+ | `expiryDate` | `string` | ISO date; `isLoggedIn` returns false after this |
61
+ | `connected` | `boolean` | Last fetch succeeded |
62
+ | `loading` | `boolean` | Fetch in progress |
63
+ | `error` | `string` | Last fetch error message |
64
+
65
+ ### `user`
66
+
67
+ `guid`, `username`, `email`, `fullNames`, `language`, `isAdmin`, `avatarUrl`, `theme`, `parameters`
68
+
69
+ ### `owner`
70
+
71
+ `id`, `guid`, `name`, `logo`, `settings`, `theme`
72
+
73
+ ### `program`
74
+
75
+ `guid`, `name`, `slug`, `environment`, `version`, `controls`, `globals`, `database`, `meta`
76
+
77
+ ### `layout`
78
+
79
+ `topBar`, `leftBar`, `rightBar`, `bottomBar` — each is `{ open, pinned, size, collapsed, menuItems }`
80
+
81
+ ### `navigation`
82
+
83
+ `menu: MenuItem[]`, `currentPage: { title, path, breadcrumbs, meta }`
84
+
85
+ ---
86
+
87
+ ## Auth lifecycle
88
+
89
+ ```
90
+ login(token)
91
+ → sets authToken, loggedIn:false, validated:false
92
+ → calls onLogin ──► server confirms → loggedIn:true, validated:true
93
+ → calls fetchData (onFetchSession) → merges server state
94
+ ```
95
+
96
+ ```
97
+ logout()
98
+ → clears all auth state, loading:true
99
+ → calls onLogout
100
+ → calls fetchData (onFetchSession)
101
+ ```
102
+
103
+ ```
104
+ fetchData(url?)
105
+ → no-op if onFetchSession not registered
106
+ → calls onFetchSession
107
+ → validates or clears session (see Session Validation below)
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Session validation
113
+
114
+ Credentials from `localStorage` start each page load as `loggedIn: true, validated: false`. `isLoggedInStore` emits `false` until validated.
115
+
116
+ Validation is triggered automatically — no manual call needed — in priority order:
117
+
118
+ 1. **`onFetchSession` registered** while unvalidated credentials exist (`setState` detects handler transition)
119
+ 2. **Any state change** with unvalidated credentials + handler present (`store.subscribe`)
120
+
121
+ `fetchData` clears the session when the server response indicates invalidity:
122
+
123
+ | Condition | Result |
124
+ |---|---|
125
+ | `onFetchSession` not registered | No-op, session unchanged |
126
+ | Server returns no session/user fields | Session preserved, `validated` stays `false` |
127
+ | Server returns `loggedIn: false` | Session cleared |
128
+ | Server returns a different `authToken` | Session cleared |
129
+ | Server returns a different `user.guid` or `username` | Session cleared |
130
+ | `expiryDate` is in the past | Session cleared |
131
+ | Server confirms credentials | `validated: true`, session kept |
132
+
133
+ ---
134
+
135
+ ## Reactive usage (Svelte components)
136
+
137
+ ```svelte
138
+ <script lang="ts">
139
+ import { isLoggedInStore, useGlobalStateStore } from '@warkypublic/svelix';
140
+
141
+ const username = useGlobalStateStore((s) => s.user.username);
142
+ </script>
143
+
144
+ {#if $isLoggedInStore}
145
+ <p>Welcome, {$username}</p>
146
+ {/if}
147
+ ```
148
+
149
+ ### Reactive stores
150
+
151
+ | Export | Type | Value |
152
+ |---|---|---|
153
+ | `isLoggedInStore` | `Readable<boolean>` | `true` only when `loggedIn && validated && !expired` |
154
+ | `languageStore` | `Readable<'en' \| 'af'>` | `user.language` |
155
+ | `useGlobalStateStore(selector?)` | `Readable<T>` | Derived slice of the full store |
156
+
157
+ ---
158
+
159
+ ## Imperative usage (outside components)
160
+
161
+ ```ts
162
+ import { GlobalStateStore, getAuthToken, getApiURL, setAuthToken, setApiURL } from '@warkypublic/svelix';
163
+
164
+ const state = GlobalStateStore.getState();
165
+
166
+ await state.login('my-token');
167
+ await state.logout();
168
+ await state.fetchData();
169
+
170
+ state.isLoggedIn(); // boolean — checks validated + loggedIn + authToken + expiry
171
+ state.setAuthToken('token');
172
+ state.setApiURL('https://api.example.com');
173
+ state.setUser({ username: 'alice' });
174
+ state.setSession({ expiryDate: '2099-01-01T00:00:00.000Z' });
175
+
176
+ getAuthToken(); // shorthand
177
+ getApiURL();
178
+ setAuthToken('token');
179
+ setApiURL('https://api.example.com');
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Persistence
185
+
186
+ All slices are written to `localStorage` under `APP_GLO:<slice>` on every state change.
187
+
188
+ **Not persisted:** `session.connected`, `session.error`, `session.loading`, `session.validated`
189
+
190
+ On load, `validated` is forced to `false` when credentials exist. `loggedIn` is preserved as-is so the UI can optimistically render the logged-in shell while re-validation completes in the background.
191
+
192
+ ---
193
+
194
+ ## Callback return shapes
195
+
196
+ ```ts
197
+ // onLogin / onLogout
198
+ Promise<{
199
+ session?: Partial<SessionState>;
200
+ user?: Partial<UserState>;
201
+ owner?: Partial<OwnerState>;
202
+ program?: Partial<ProgramState>;
203
+ } | void>
204
+
205
+ // onFetchSession (superset — also accepts layout + navigation)
206
+ Promise<Partial<GlobalState>>
207
+ ```
208
+
209
+ Return only the fields that changed. Omitted fields are left as-is.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warkypublic/svelix",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Svelte 5 component library with Skeleton UI and Tailwind CSS",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {