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