@inventbuild/supamachine 0.3.0
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.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/core/constants.d.ts +33 -0
- package/dist/core/constants.js +31 -0
- package/dist/core/events.d.ts +26 -0
- package/dist/core/events.js +1 -0
- package/dist/core/logger.d.ts +15 -0
- package/dist/core/logger.js +41 -0
- package/dist/core/reducer.d.ts +5 -0
- package/dist/core/reducer.js +223 -0
- package/dist/core/runtime.d.ts +40 -0
- package/dist/core/runtime.js +177 -0
- package/dist/core/states.d.ts +42 -0
- package/dist/core/states.js +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/react/SupamachineProvider.d.ts +32 -0
- package/dist/react/SupamachineProvider.js +78 -0
- package/dist/react/useSupamachine.d.ts +1 -0
- package/dist/react/useSupamachine.js +1 -0
- package/dist/src/core/constants.d.ts +31 -0
- package/dist/src/core/events.d.ts +25 -0
- package/dist/src/core/logger.d.ts +15 -0
- package/dist/src/core/reducer.d.ts +5 -0
- package/dist/src/core/runtime.d.ts +39 -0
- package/dist/src/core/states.d.ts +32 -0
- package/dist/src/core/types.d.ts +33 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/react/SupamachineProvider.d.ts +28 -0
- package/dist/src/react/useSupamachine.d.ts +1 -0
- package/dist/src/supabase/adapter.d.ts +7 -0
- package/dist/supabase/adapter.d.ts +7 -0
- package/dist/supabase/adapter.js +66 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InventBuild.Studio
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Supamachine
|
|
2
|
+
|
|
3
|
+
A deterministic authentication state machine for Supabase web and mobile apps.
|
|
4
|
+
|
|
5
|
+
This is an early-stage library designed to make authentication easier and way less error-prone, especially as your app grows in complexity.
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Does this kind of auth code look familiar to you?
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const {
|
|
13
|
+
data: { subscription },
|
|
14
|
+
} = supabase.auth.onAuthStateChange((event, session) => {
|
|
15
|
+
if (event === "INITIAL_SESSION" || event === "TOKEN_REFRESHED") {
|
|
16
|
+
setLoading(false);
|
|
17
|
+
}
|
|
18
|
+
if (session) {
|
|
19
|
+
setSession(session);
|
|
20
|
+
} else {
|
|
21
|
+
setSession(null);
|
|
22
|
+
}
|
|
23
|
+
...and so much more of this
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**THERE'S GOT TO BE A BETTER WAY!**
|
|
28
|
+
|
|
29
|
+
Supamachine models auth as an explicit state machine with clear states (CHECKING, SIGNED_OUT, CONTEXT_LOADING, INITIALIZING, AUTH_READY, plus error states) and allows you to derive custom app states via `mapState`.
|
|
30
|
+
|
|
31
|
+
## Real-World Benefits
|
|
32
|
+
|
|
33
|
+
When I moved a client project from my original AuthContext to Supamachine, Supamachine turned ~300 lines of lifecycle orchestration (session management, auth state changes, post-login flow, navigation decisions) into ~50 lines of configuration (loadContext, initializeApp, mapState).
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
`pnpm add @inventbuild/supamachine`
|
|
38
|
+
|
|
39
|
+
### Basic setup
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import {
|
|
43
|
+
SupamachineProvider,
|
|
44
|
+
useSupamachine,
|
|
45
|
+
AuthStateStatus,
|
|
46
|
+
} from "@inventbuild/supamachine";
|
|
47
|
+
|
|
48
|
+
type MyContext = { userData: { name: string } };
|
|
49
|
+
type MyAppState = { status: "MAIN_APP"; session: Session; context: MyContext };
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<SupamachineProvider<MyContext, MyAppState>
|
|
53
|
+
supabase={supabase}
|
|
54
|
+
loadContext={async (session) => {
|
|
55
|
+
const { data } = await supabase
|
|
56
|
+
.from("profiles")
|
|
57
|
+
.select("*")
|
|
58
|
+
.eq("id", session.user.id)
|
|
59
|
+
.single();
|
|
60
|
+
return { userData: data };
|
|
61
|
+
}}
|
|
62
|
+
mapState={(snapshot) => ({
|
|
63
|
+
status: "MAIN_APP",
|
|
64
|
+
session: snapshot.session,
|
|
65
|
+
context: snapshot.context!,
|
|
66
|
+
})}
|
|
67
|
+
>
|
|
68
|
+
<App />
|
|
69
|
+
</SupamachineProvider>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
function App() {
|
|
73
|
+
const { state, updateContext } = useSupamachine<MyContext, MyAppState>();
|
|
74
|
+
|
|
75
|
+
switch (state.status) {
|
|
76
|
+
case AuthStateStatus.CHECKING:
|
|
77
|
+
case AuthStateStatus.CONTEXT_LOADING:
|
|
78
|
+
return <Loading />;
|
|
79
|
+
case AuthStateStatus.SIGNED_OUT:
|
|
80
|
+
return <Login />;
|
|
81
|
+
case "MAIN_APP":
|
|
82
|
+
return <Home session={state.session} />;
|
|
83
|
+
default:
|
|
84
|
+
return <Loading />;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Provider API
|
|
90
|
+
|
|
91
|
+
- **supabase** (required) – Supabase client instance
|
|
92
|
+
- **loadContext(session)** – Optional. Fetches app context (e.g. user profile) after auth
|
|
93
|
+
- **initializeApp({ session, context })** – Optional. Side effects after context is loaded (e.g. set avatar)
|
|
94
|
+
- **mapState(snapshot)** – Optional. Maps the internal AUTH_READY state to your custom app states
|
|
95
|
+
- **actions** – Optional. Auth actions (signIn, signOut, etc.) to expose via `useSupamachine()`. Merged with a default `signOut` so you always have `actions.signOut()` available.
|
|
96
|
+
- **options** – Optional. `logLevel`, `getSessionTimeoutMs`, `loadContextTimeoutMs`, `initializeAppTimeoutMs`
|
|
97
|
+
|
|
98
|
+
### actions
|
|
99
|
+
|
|
100
|
+
This is an optional convenience for your imperative Supabase auth methods, like signInWith... and signOut, etc. Pass your auth actions to the provider; they're exposed via `useSupamachine().actions`. Since Supamachine responds to Supabase events, you don't need to use updateContext. A default `signOut` is included since it's simple (but can be overriden). Example usage:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
<SupamachineProvider
|
|
104
|
+
supabase={supabase}
|
|
105
|
+
actions={{
|
|
106
|
+
signOut: () => supabase.auth.signOut(),
|
|
107
|
+
signInWithOtp: (email) => supabase.auth.signInWithOtp({ email }),
|
|
108
|
+
signInWithGoogle: () => { /* platform-specific */ },
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const { state, actions } = useSupamachine();
|
|
115
|
+
actions.signOut();
|
|
116
|
+
actions.signInWithOtp("user@example.com");
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If you omit `actions`, you still get `actions.signOut()` from the default.
|
|
120
|
+
|
|
121
|
+
### updateContext
|
|
122
|
+
|
|
123
|
+
Use `updateContext` to imperatively update context and trigger a re-run of `mapState`:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const { updateContext } = useSupamachine();
|
|
127
|
+
updateContext((current) => ({
|
|
128
|
+
...current,
|
|
129
|
+
userData: { ...current.userData, onboardingComplete: true },
|
|
130
|
+
}));
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Philosophy
|
|
134
|
+
|
|
135
|
+
Handling auth in your app is all about _states._ Supamachine explicitly defines every possible state (CHECKING, SIGNED*OUT, CONTEXT_LOADING, INITIALIZING, AUTH_READY, ERROR*\*) and lets you extend with custom states via `mapState`. By capturing all states and transitions, edge cases are handled deterministically.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core machine states. Use these when checking state or in mapState.
|
|
3
|
+
* Distinct from AuthEventType (event types that drive transitions).
|
|
4
|
+
*/
|
|
5
|
+
export declare const AuthStateStatus: {
|
|
6
|
+
readonly START: "START";
|
|
7
|
+
readonly CHECKING_SESSION: "CHECKING_SESSION";
|
|
8
|
+
readonly AUTHENTICATING: "AUTHENTICATING";
|
|
9
|
+
readonly SIGNED_OUT: "SIGNED_OUT";
|
|
10
|
+
readonly CONTEXT_LOADING: "CONTEXT_LOADING";
|
|
11
|
+
readonly INITIALIZING: "INITIALIZING";
|
|
12
|
+
readonly AUTH_READY: "AUTH_READY";
|
|
13
|
+
readonly ERROR_CHECKING_SESSION: "ERROR_CHECKING_SESSION";
|
|
14
|
+
readonly ERROR_CONTEXT: "ERROR_CONTEXT";
|
|
15
|
+
readonly ERROR_INITIALIZING: "ERROR_INITIALIZING";
|
|
16
|
+
};
|
|
17
|
+
export type AuthStateStatus = (typeof AuthStateStatus)[keyof typeof AuthStateStatus];
|
|
18
|
+
/**
|
|
19
|
+
* Internal event types that drive the state machine.
|
|
20
|
+
* Mapped from Supabase auth events by the adapter.
|
|
21
|
+
*/
|
|
22
|
+
export declare const AuthEventType: {
|
|
23
|
+
readonly START: "START";
|
|
24
|
+
readonly AUTH_CHANGED: "AUTH_CHANGED";
|
|
25
|
+
readonly AUTH_INITIATED: "AUTH_INITIATED";
|
|
26
|
+
readonly AUTH_CANCELLED: "AUTH_CANCELLED";
|
|
27
|
+
readonly CONTEXT_RESOLVED: "CONTEXT_RESOLVED";
|
|
28
|
+
readonly INITIALIZED: "INITIALIZED";
|
|
29
|
+
readonly ERROR_CHECKING_SESSION: "ERROR_CHECKING_SESSION";
|
|
30
|
+
readonly ERROR_CONTEXT: "ERROR_CONTEXT";
|
|
31
|
+
readonly ERROR_INITIALIZING: "ERROR_INITIALIZING";
|
|
32
|
+
};
|
|
33
|
+
export type AuthEventType = (typeof AuthEventType)[keyof typeof AuthEventType];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core machine states. Use these when checking state or in mapState.
|
|
3
|
+
* Distinct from AuthEventType (event types that drive transitions).
|
|
4
|
+
*/
|
|
5
|
+
export const AuthStateStatus = {
|
|
6
|
+
START: "START",
|
|
7
|
+
CHECKING_SESSION: "CHECKING_SESSION",
|
|
8
|
+
AUTHENTICATING: "AUTHENTICATING",
|
|
9
|
+
SIGNED_OUT: "SIGNED_OUT",
|
|
10
|
+
CONTEXT_LOADING: "CONTEXT_LOADING",
|
|
11
|
+
INITIALIZING: "INITIALIZING",
|
|
12
|
+
AUTH_READY: "AUTH_READY",
|
|
13
|
+
ERROR_CHECKING_SESSION: "ERROR_CHECKING_SESSION",
|
|
14
|
+
ERROR_CONTEXT: "ERROR_CONTEXT",
|
|
15
|
+
ERROR_INITIALIZING: "ERROR_INITIALIZING",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Internal event types that drive the state machine.
|
|
19
|
+
* Mapped from Supabase auth events by the adapter.
|
|
20
|
+
*/
|
|
21
|
+
export const AuthEventType = {
|
|
22
|
+
START: "START",
|
|
23
|
+
AUTH_CHANGED: "AUTH_CHANGED",
|
|
24
|
+
AUTH_INITIATED: "AUTH_INITIATED",
|
|
25
|
+
AUTH_CANCELLED: "AUTH_CANCELLED",
|
|
26
|
+
CONTEXT_RESOLVED: "CONTEXT_RESOLVED",
|
|
27
|
+
INITIALIZED: "INITIALIZED",
|
|
28
|
+
ERROR_CHECKING_SESSION: "ERROR_CHECKING_SESSION",
|
|
29
|
+
ERROR_CONTEXT: "ERROR_CONTEXT",
|
|
30
|
+
ERROR_INITIALIZING: "ERROR_INITIALIZING",
|
|
31
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Session } from "@supabase/supabase-js";
|
|
2
|
+
import { AuthEventType } from "./constants";
|
|
3
|
+
export type AuthEvent<C> = {
|
|
4
|
+
type: typeof AuthEventType.START;
|
|
5
|
+
} | {
|
|
6
|
+
type: typeof AuthEventType.AUTH_CHANGED;
|
|
7
|
+
session: Session | null;
|
|
8
|
+
} | {
|
|
9
|
+
type: typeof AuthEventType.AUTH_INITIATED;
|
|
10
|
+
} | {
|
|
11
|
+
type: typeof AuthEventType.AUTH_CANCELLED;
|
|
12
|
+
} | {
|
|
13
|
+
type: typeof AuthEventType.CONTEXT_RESOLVED;
|
|
14
|
+
context: C;
|
|
15
|
+
} | {
|
|
16
|
+
type: typeof AuthEventType.INITIALIZED;
|
|
17
|
+
} | {
|
|
18
|
+
type: typeof AuthEventType.ERROR_CHECKING_SESSION;
|
|
19
|
+
error: Error;
|
|
20
|
+
} | {
|
|
21
|
+
type: typeof AuthEventType.ERROR_CONTEXT;
|
|
22
|
+
error: Error;
|
|
23
|
+
} | {
|
|
24
|
+
type: typeof AuthEventType.ERROR_INITIALIZING;
|
|
25
|
+
error: Error;
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const LogLevel: {
|
|
2
|
+
readonly NONE: 0;
|
|
3
|
+
readonly ERROR: 1;
|
|
4
|
+
readonly WARN: 2;
|
|
5
|
+
readonly INFO: 3;
|
|
6
|
+
readonly DEBUG: 4;
|
|
7
|
+
};
|
|
8
|
+
export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
|
|
9
|
+
export declare function createLogger(subsystem: string, level: LogLevel): {
|
|
10
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
11
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
12
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
13
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function parseLogLevel(value: string | undefined): LogLevel;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const LogLevel = {
|
|
2
|
+
NONE: 0,
|
|
3
|
+
ERROR: 1,
|
|
4
|
+
WARN: 2,
|
|
5
|
+
INFO: 3,
|
|
6
|
+
DEBUG: 4,
|
|
7
|
+
};
|
|
8
|
+
const PREFIX = '[Supamachine]';
|
|
9
|
+
export function createLogger(subsystem, level) {
|
|
10
|
+
const prefix = `${PREFIX}[${subsystem}]`;
|
|
11
|
+
return {
|
|
12
|
+
error: level >= LogLevel.ERROR
|
|
13
|
+
? (msg, ...a) => console.error(prefix, msg, ...a)
|
|
14
|
+
: () => { },
|
|
15
|
+
warn: level >= LogLevel.WARN
|
|
16
|
+
? (msg, ...a) => console.warn(prefix, msg, ...a)
|
|
17
|
+
: () => { },
|
|
18
|
+
info: level >= LogLevel.INFO
|
|
19
|
+
? (msg, ...a) => console.log(prefix, msg, ...a)
|
|
20
|
+
: () => { },
|
|
21
|
+
debug: level >= LogLevel.DEBUG
|
|
22
|
+
? (msg, ...a) => console.log(prefix, msg, ...a)
|
|
23
|
+
: () => { },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function parseLogLevel(value) {
|
|
27
|
+
if (!value)
|
|
28
|
+
return LogLevel.WARN;
|
|
29
|
+
const v = value.toLowerCase();
|
|
30
|
+
if (v === 'none')
|
|
31
|
+
return LogLevel.NONE;
|
|
32
|
+
if (v === 'error')
|
|
33
|
+
return LogLevel.ERROR;
|
|
34
|
+
if (v === 'warn')
|
|
35
|
+
return LogLevel.WARN;
|
|
36
|
+
if (v === 'info')
|
|
37
|
+
return LogLevel.INFO;
|
|
38
|
+
if (v === 'debug')
|
|
39
|
+
return LogLevel.DEBUG;
|
|
40
|
+
return LogLevel.WARN;
|
|
41
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CoreState } from "./states";
|
|
2
|
+
import type { AuthEvent } from "./events";
|
|
3
|
+
import { type LogLevel } from "./logger";
|
|
4
|
+
export declare function setReducerLogLevel(level: LogLevel): void;
|
|
5
|
+
export declare function reducer<C>(state: CoreState<C>, event: AuthEvent<C>): CoreState<C>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { AuthStateStatus, AuthEventType } from "./constants";
|
|
2
|
+
import { createLogger, LogLevel as LL } from "./logger";
|
|
3
|
+
let log = createLogger("reducer", LL.WARN);
|
|
4
|
+
export function setReducerLogLevel(level) {
|
|
5
|
+
log = createLogger("reducer", level);
|
|
6
|
+
}
|
|
7
|
+
/** Pre-context states use null; avoids TS inferring C = null from literal */
|
|
8
|
+
const NO_CONTEXT = null;
|
|
9
|
+
function invalidTransition(state, event) {
|
|
10
|
+
log.warn(`invalid transition: ${state.status} + ${event.type}`);
|
|
11
|
+
return state;
|
|
12
|
+
}
|
|
13
|
+
export function reducer(state, event) {
|
|
14
|
+
let next;
|
|
15
|
+
switch (state.status) {
|
|
16
|
+
case AuthStateStatus.START:
|
|
17
|
+
switch (event.type) {
|
|
18
|
+
case AuthEventType.START:
|
|
19
|
+
next = { status: AuthStateStatus.CHECKING_SESSION, context: NO_CONTEXT };
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
return invalidTransition(state, event);
|
|
23
|
+
}
|
|
24
|
+
break;
|
|
25
|
+
case AuthStateStatus.CHECKING_SESSION:
|
|
26
|
+
switch (event.type) {
|
|
27
|
+
case AuthEventType.AUTH_CHANGED:
|
|
28
|
+
if (event.session) {
|
|
29
|
+
next = {
|
|
30
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
31
|
+
session: event.session,
|
|
32
|
+
context: NO_CONTEXT,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
case AuthEventType.ERROR_CHECKING_SESSION:
|
|
40
|
+
next = {
|
|
41
|
+
status: AuthStateStatus.ERROR_CHECKING_SESSION,
|
|
42
|
+
error: event.error,
|
|
43
|
+
context: NO_CONTEXT,
|
|
44
|
+
};
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
return invalidTransition(state, event);
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
case AuthStateStatus.AUTHENTICATING:
|
|
51
|
+
switch (event.type) {
|
|
52
|
+
case AuthEventType.AUTH_CHANGED:
|
|
53
|
+
if (event.session) {
|
|
54
|
+
next = {
|
|
55
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
56
|
+
session: event.session,
|
|
57
|
+
context: NO_CONTEXT,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
case AuthEventType.AUTH_CANCELLED:
|
|
65
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
return invalidTransition(state, event);
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
case AuthStateStatus.CONTEXT_LOADING:
|
|
72
|
+
switch (event.type) {
|
|
73
|
+
case AuthEventType.CONTEXT_RESOLVED:
|
|
74
|
+
next = {
|
|
75
|
+
status: AuthStateStatus.INITIALIZING,
|
|
76
|
+
session: state.session,
|
|
77
|
+
context: event.context,
|
|
78
|
+
};
|
|
79
|
+
break;
|
|
80
|
+
case AuthEventType.ERROR_CONTEXT:
|
|
81
|
+
next = {
|
|
82
|
+
status: AuthStateStatus.ERROR_CONTEXT,
|
|
83
|
+
session: state.session,
|
|
84
|
+
error: event.error,
|
|
85
|
+
context: NO_CONTEXT,
|
|
86
|
+
};
|
|
87
|
+
break;
|
|
88
|
+
case AuthEventType.AUTH_CHANGED:
|
|
89
|
+
if (!event.session) {
|
|
90
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
next = {
|
|
94
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
95
|
+
session: event.session,
|
|
96
|
+
context: NO_CONTEXT,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
default:
|
|
101
|
+
return invalidTransition(state, event);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case AuthStateStatus.INITIALIZING:
|
|
105
|
+
switch (event.type) {
|
|
106
|
+
case AuthEventType.INITIALIZED:
|
|
107
|
+
next = {
|
|
108
|
+
status: AuthStateStatus.AUTH_READY,
|
|
109
|
+
session: state.session,
|
|
110
|
+
context: state.context,
|
|
111
|
+
};
|
|
112
|
+
break;
|
|
113
|
+
case AuthEventType.ERROR_INITIALIZING:
|
|
114
|
+
next = {
|
|
115
|
+
status: AuthStateStatus.ERROR_INITIALIZING,
|
|
116
|
+
error: event.error,
|
|
117
|
+
session: state.session,
|
|
118
|
+
context: state.context,
|
|
119
|
+
};
|
|
120
|
+
break;
|
|
121
|
+
case AuthEventType.AUTH_CHANGED:
|
|
122
|
+
if (!event.session) {
|
|
123
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
next = {
|
|
127
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
128
|
+
session: event.session,
|
|
129
|
+
context: NO_CONTEXT,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
return invalidTransition(state, event);
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
case AuthStateStatus.AUTH_READY:
|
|
138
|
+
switch (event.type) {
|
|
139
|
+
case AuthEventType.AUTH_CHANGED:
|
|
140
|
+
if (!event.session) {
|
|
141
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
next = {
|
|
145
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
146
|
+
session: event.session,
|
|
147
|
+
context: NO_CONTEXT,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
return invalidTransition(state, event);
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case AuthStateStatus.SIGNED_OUT:
|
|
156
|
+
switch (event.type) {
|
|
157
|
+
case AuthEventType.AUTH_CHANGED:
|
|
158
|
+
if (event.session) {
|
|
159
|
+
next = {
|
|
160
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
161
|
+
session: event.session,
|
|
162
|
+
context: NO_CONTEXT,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
next = state;
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
case AuthEventType.AUTH_INITIATED:
|
|
170
|
+
next = { status: AuthStateStatus.AUTHENTICATING, context: NO_CONTEXT };
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
return invalidTransition(state, event);
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case AuthStateStatus.ERROR_CHECKING_SESSION:
|
|
177
|
+
switch (event.type) {
|
|
178
|
+
case AuthEventType.AUTH_CHANGED:
|
|
179
|
+
if (event.session) {
|
|
180
|
+
next = {
|
|
181
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
182
|
+
session: event.session,
|
|
183
|
+
context: NO_CONTEXT,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case AuthEventType.AUTH_INITIATED:
|
|
191
|
+
next = { status: AuthStateStatus.AUTHENTICATING, context: NO_CONTEXT };
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
return invalidTransition(state, event);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case AuthStateStatus.ERROR_CONTEXT:
|
|
198
|
+
case AuthStateStatus.ERROR_INITIALIZING:
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case AuthEventType.AUTH_CHANGED:
|
|
201
|
+
if (event.session) {
|
|
202
|
+
next = {
|
|
203
|
+
status: AuthStateStatus.CONTEXT_LOADING,
|
|
204
|
+
session: event.session,
|
|
205
|
+
context: NO_CONTEXT,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
next = { status: AuthStateStatus.SIGNED_OUT, context: NO_CONTEXT };
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
return invalidTransition(state, event);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
default: {
|
|
217
|
+
const _exhaustive = state;
|
|
218
|
+
throw new Error(`unknown state: ${state.status}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
log.debug(`${state.status} + ${event.type} → ${next.status}`);
|
|
222
|
+
return next;
|
|
223
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Session } from "@supabase/supabase-js";
|
|
2
|
+
import type { CoreState } from "./states";
|
|
3
|
+
import type { AuthEvent } from "./events";
|
|
4
|
+
import type { AppState, MapStateSnapshot } from "./types";
|
|
5
|
+
import { type LogLevel } from "./logger";
|
|
6
|
+
export interface RuntimeOptions<C, D> {
|
|
7
|
+
loadContext?: (session: Session) => Promise<C>;
|
|
8
|
+
mapState?: (snapshot: MapStateSnapshot<C>) => D;
|
|
9
|
+
initializeApp?: (snapshot: {
|
|
10
|
+
session: Session;
|
|
11
|
+
context: C;
|
|
12
|
+
}) => void | Promise<void>;
|
|
13
|
+
loadContextTimeoutMs?: number;
|
|
14
|
+
initializeAppTimeoutMs?: number;
|
|
15
|
+
authenticatingTimeoutMs?: number;
|
|
16
|
+
logLevel?: LogLevel;
|
|
17
|
+
}
|
|
18
|
+
export declare class SupamachineCore<C, D> {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private state;
|
|
21
|
+
private sessionForLoading;
|
|
22
|
+
private authenticatingTimer;
|
|
23
|
+
private listeners;
|
|
24
|
+
private loadContextTimeoutMs;
|
|
25
|
+
private initializeAppTimeoutMs;
|
|
26
|
+
private authenticatingTimeoutMs;
|
|
27
|
+
private log;
|
|
28
|
+
constructor(options: RuntimeOptions<C, D>);
|
|
29
|
+
setLogLevel(level: LogLevel): void;
|
|
30
|
+
updateContext(updater: (current: C) => C | Promise<C>): Promise<void>;
|
|
31
|
+
beginAuth(): void;
|
|
32
|
+
cancelAuth(): void;
|
|
33
|
+
dispatch(event: AuthEvent<C>): void;
|
|
34
|
+
private loadContextWithTimeout;
|
|
35
|
+
private initializeAppWithTimeout;
|
|
36
|
+
subscribe(fn: (coreState: CoreState<C>, appState: AppState<C, D>) => void): () => boolean;
|
|
37
|
+
getSnapshot(): CoreState<C>;
|
|
38
|
+
getAppState(): AppState<C, D>;
|
|
39
|
+
private emit;
|
|
40
|
+
}
|