@warkypublic/svelix 0.1.33 → 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,20 +102,50 @@ 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();
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
+ };
110
128
  const store = writable(undefined);
111
129
  const getState = () => get(store);
112
130
  const setState = (partial, replace = false) => {
131
+ const before = get(store);
113
132
  store.update((current) => {
114
133
  const nextPartial = typeof partial === "function" ? partial(current) : partial;
115
134
  return replace
116
135
  ? nextPartial
117
136
  : { ...current, ...nextPartial };
118
137
  });
138
+ const after = get(store);
139
+ const handlerJustRegistered = !hasAutoValidated &&
140
+ typeof after.onFetchSession === "function" &&
141
+ typeof before?.onFetchSession !== "function";
142
+ if (handlerJustRegistered) {
143
+ hasAutoValidated = true;
144
+ const session = after.session;
145
+ if (hasSessionCredentials(session) && !session.validated) {
146
+ scheduleValidation();
147
+ }
148
+ }
119
149
  };
120
150
  const setGlobalState = (partial) => {
121
151
  setState((state) => {
@@ -152,8 +182,8 @@ const createGlobalStateStore = () => {
152
182
  }));
153
183
  try {
154
184
  const currentState = getState();
185
+ const hasFetchHandler = typeof currentState.onFetchSession === "function";
155
186
  const result = await currentState.onFetchSession?.(currentState);
156
- const hasExistingSession = hasSessionCredentials(currentState.session);
157
187
  const nextUser = { ...currentState.user, ...result?.user };
158
188
  const nextSession = {
159
189
  ...currentState.session,
@@ -162,6 +192,14 @@ const createGlobalStateStore = () => {
162
192
  connected: true,
163
193
  loading: false,
164
194
  };
195
+ if (!hasFetchHandler) {
196
+ setGlobalState((state) => ({
197
+ ...state,
198
+ session: { ...state.session, connected: true, loading: false },
199
+ }));
200
+ return;
201
+ }
202
+ const hasExistingSession = hasSessionCredentials(currentState.session);
165
203
  const serverConfirmsSession = result?.session?.loggedIn === true || Boolean(result?.session?.authToken);
166
204
  const serverConfirmsUser = Boolean(result?.user?.guid || result?.user?.username);
167
205
  const alreadyValidated = currentState.session.validated === true;
@@ -237,16 +275,7 @@ const createGlobalStateStore = () => {
237
275
  await waitForInitialization();
238
276
  return withOperationLock(() => fetchDataInternal(url));
239
277
  },
240
- isLoggedIn: () => {
241
- const session = getState().session;
242
- if (!session.validated || !session.loggedIn || !session.authToken) {
243
- return false;
244
- }
245
- if (isSessionExpired(session)) {
246
- return false;
247
- }
248
- return true;
249
- },
278
+ isLoggedIn: () => computeIsLoggedIn(getState().session),
250
279
  login: async (authToken, user) => {
251
280
  await waitForInitialization();
252
281
  return withOperationLock(async () => {
@@ -442,9 +471,6 @@ const createGlobalStateStore = () => {
442
471
  ...loadedState.session,
443
472
  connected: true,
444
473
  loading: false,
445
- loggedIn: hasPersistedSession
446
- ? false
447
- : loadedState.session?.loggedIn ?? current.session.loggedIn,
448
474
  validated: hasPersistedSession
449
475
  ? false
450
476
  : loadedState.session?.validated ?? current.session.validated,
@@ -463,6 +489,12 @@ const createGlobalStateStore = () => {
463
489
  if (!isStorageInitialized) {
464
490
  return;
465
491
  }
492
+ if (hasSessionCredentials(state.session) &&
493
+ !state.session.validated &&
494
+ !computeIsLoggedIn(state.session) &&
495
+ typeof state.onFetchSession === "function") {
496
+ scheduleValidation();
497
+ }
466
498
  saveStorage(toPersistedState(state)).catch((e) => {
467
499
  console.error("Error saving storage:", e);
468
500
  });
@@ -478,6 +510,7 @@ const GlobalStateStoreReadable = {
478
510
  subscribe: GlobalStateStore.subscribe,
479
511
  };
480
512
  const languageStore = derived(GlobalStateStoreReadable, (state) => state.user.language ?? "en");
513
+ const isLoggedInStore = derived(GlobalStateStoreReadable, (state) => computeIsLoggedIn(state.session));
481
514
  initTranslation(languageStore);
482
515
  function useGlobalStateStore(selector) {
483
516
  if (!selector) {
@@ -494,9 +527,6 @@ const getApiURL = () => {
494
527
  const getAuthToken = () => {
495
528
  return GlobalStateStore.getState().session.authToken ?? "";
496
529
  };
497
- const isLoggedIn = () => {
498
- return GlobalStateStore.getState().isLoggedIn();
499
- };
500
530
  const setAuthToken = (token) => {
501
531
  GlobalStateStore.getState().setAuthToken(token);
502
532
  };
@@ -506,4 +536,4 @@ const GetGlobalState = () => {
506
536
  const setLanguage = (lang) => {
507
537
  GlobalStateStore.getState().setLanguage(lang);
508
538
  };
509
- 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.33",
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": {