@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,
|
|
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
|
-
|
|
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,
|
|
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.
|