@unlaxer/tramli-react 0.1.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/README.md +61 -0
- package/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/use-flow.d.ts +28 -0
- package/dist/cjs/use-flow.js +85 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/use-flow.d.ts +28 -0
- package/dist/esm/use-flow.js +82 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @unlaxer/tramli-react
|
|
2
|
+
|
|
3
|
+
tramli の React フック。`useFlow` が FlowEngine + FlowStore + FlowInstance のライフサイクルをコンポーネントに統合します。
|
|
4
|
+
|
|
5
|
+
## インストール
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @unlaxer/tramli-react @unlaxer/tramli react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 使い方
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useFlow } from '@unlaxer/tramli-react';
|
|
15
|
+
|
|
16
|
+
function AuthPage() {
|
|
17
|
+
const { state, context, resume, error, isLoading, flowId } = useFlow(authFlowDefinition, {
|
|
18
|
+
initialData: new Map([['RequestOrigin', { returnTo: '/' }]]),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (isLoading) return <Spinner />;
|
|
22
|
+
if (error) return <ErrorBanner error={error} />;
|
|
23
|
+
if (state === 'MFA_PENDING') return <MfaForm onSubmit={code => resume(new Map([['MfaCode', code]]))} />;
|
|
24
|
+
if (state === 'COMPLETE') return <Dashboard />;
|
|
25
|
+
return <LoginRedirect />;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### `useFlow<S>(definition, options?)`
|
|
32
|
+
|
|
33
|
+
| 引数 | 型 | 説明 |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `definition` | `FlowDefinition<S>` | tramli フロー定義 |
|
|
36
|
+
| `options.initialData` | `Map<string, unknown>` | 初期データ |
|
|
37
|
+
| `options.sessionId` | `string` | セッション ID (省略時は自動生成) |
|
|
38
|
+
|
|
39
|
+
### 戻り値 (`UseFlowResult<S>`)
|
|
40
|
+
|
|
41
|
+
| フィールド | 型 | 説明 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `state` | `S \| null` | 現在のフロー状態 |
|
|
44
|
+
| `context` | `FlowContext \| null` | フローコンテキスト |
|
|
45
|
+
| `flowId` | `string \| null` | フローインスタンス ID |
|
|
46
|
+
| `error` | `Error \| null` | 直近のエラー |
|
|
47
|
+
| `isLoading` | `boolean` | startFlow/resume 実行中 |
|
|
48
|
+
| `resume` | `(data?) => Promise<void>` | 外部データでフローを再開 |
|
|
49
|
+
|
|
50
|
+
## 動作
|
|
51
|
+
|
|
52
|
+
- `FlowEngine` + `InMemoryFlowStore` はマウント時に一度だけ生成 (`useRef`)
|
|
53
|
+
- `startFlow` は `useEffect` で非同期実行
|
|
54
|
+
- `resume` は `useCallback` で安定参照
|
|
55
|
+
- 各操作後に `FlowInstance` から React state を同期
|
|
56
|
+
- エラーは `error` フィールドに格納 (throw しない)
|
|
57
|
+
- 操作中は `isLoading = true`
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFlow = void 0;
|
|
4
|
+
var use_flow_js_1 = require("./use-flow.js");
|
|
5
|
+
Object.defineProperty(exports, "useFlow", { enumerable: true, get: function () { return use_flow_js_1.useFlow; } });
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FlowDefinition, type FlowContext } from '@unlaxer/tramli';
|
|
2
|
+
export interface UseFlowOptions {
|
|
3
|
+
/** Initial data to seed the flow context. */
|
|
4
|
+
initialData?: Map<string, unknown>;
|
|
5
|
+
/** Session ID for the flow instance. Defaults to crypto.randomUUID(). */
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UseFlowResult<S extends string> {
|
|
9
|
+
/** Current flow state, or null before the flow starts. */
|
|
10
|
+
state: S | null;
|
|
11
|
+
/** Flow context, or null before the flow starts. */
|
|
12
|
+
context: FlowContext | null;
|
|
13
|
+
/** Flow instance ID, or null before the flow starts. */
|
|
14
|
+
flowId: string | null;
|
|
15
|
+
/** Error from the last operation, or null. */
|
|
16
|
+
error: Error | null;
|
|
17
|
+
/** True while startFlow or resume is in progress. */
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
/** Resume the flow with optional external data. */
|
|
20
|
+
resume: (externalData?: Map<string, unknown>) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* React hook that manages a tramli flow lifecycle.
|
|
24
|
+
*
|
|
25
|
+
* Creates a FlowEngine + InMemoryFlowStore once per mount,
|
|
26
|
+
* starts the flow in useEffect, and exposes state/context/resume.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useFlow<S extends string>(definition: FlowDefinition<S>, options?: UseFlowOptions): UseFlowResult<S>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFlow = useFlow;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const tramli_1 = require("@unlaxer/tramli");
|
|
6
|
+
/**
|
|
7
|
+
* React hook that manages a tramli flow lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* Creates a FlowEngine + InMemoryFlowStore once per mount,
|
|
10
|
+
* starts the flow in useEffect, and exposes state/context/resume.
|
|
11
|
+
*/
|
|
12
|
+
function useFlow(definition, options) {
|
|
13
|
+
const [state, setState] = (0, react_1.useState)(null);
|
|
14
|
+
const [context, setContext] = (0, react_1.useState)(null);
|
|
15
|
+
const [flowId, setFlowId] = (0, react_1.useState)(null);
|
|
16
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
17
|
+
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
18
|
+
// Singleton store + engine per mount
|
|
19
|
+
const storeRef = (0, react_1.useRef)(null);
|
|
20
|
+
const engineRef = (0, react_1.useRef)(null);
|
|
21
|
+
const flowIdRef = (0, react_1.useRef)(null);
|
|
22
|
+
if (storeRef.current === null) {
|
|
23
|
+
storeRef.current = new tramli_1.InMemoryFlowStore();
|
|
24
|
+
engineRef.current = tramli_1.Tramli.engine(storeRef.current);
|
|
25
|
+
}
|
|
26
|
+
// Sync React state from a flow instance
|
|
27
|
+
const syncFromInstance = (0, react_1.useCallback)((instance) => {
|
|
28
|
+
setState(instance.currentState);
|
|
29
|
+
setContext(instance.context);
|
|
30
|
+
setFlowId(instance.id);
|
|
31
|
+
flowIdRef.current = instance.id;
|
|
32
|
+
}, []);
|
|
33
|
+
// Start flow on mount
|
|
34
|
+
(0, react_1.useEffect)(() => {
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
async function start() {
|
|
37
|
+
try {
|
|
38
|
+
setIsLoading(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
const sessionId = options?.sessionId ?? crypto.randomUUID();
|
|
41
|
+
const initialData = options?.initialData ?? new Map();
|
|
42
|
+
const instance = await engineRef.current.startFlow(definition, sessionId, initialData);
|
|
43
|
+
if (!cancelled) {
|
|
44
|
+
syncFromInstance(instance);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (!cancelled) {
|
|
49
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (!cancelled) {
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
start();
|
|
59
|
+
return () => {
|
|
60
|
+
cancelled = true;
|
|
61
|
+
};
|
|
62
|
+
// definition identity is stable per mount — callers should useMemo if needed
|
|
63
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
|
+
}, [definition]);
|
|
65
|
+
// Stable resume callback
|
|
66
|
+
const resume = (0, react_1.useCallback)(async (externalData) => {
|
|
67
|
+
const currentFlowId = flowIdRef.current;
|
|
68
|
+
if (!currentFlowId || !engineRef.current) {
|
|
69
|
+
throw new Error('Flow not started yet — cannot resume');
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
setIsLoading(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
const instance = await engineRef.current.resumeAndExecute(currentFlowId, definition, externalData);
|
|
75
|
+
syncFromInstance(instance);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}, [definition, syncFromInstance]);
|
|
84
|
+
return { state, context, flowId, error, isLoading, resume };
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useFlow } from './use-flow.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FlowDefinition, type FlowContext } from '@unlaxer/tramli';
|
|
2
|
+
export interface UseFlowOptions {
|
|
3
|
+
/** Initial data to seed the flow context. */
|
|
4
|
+
initialData?: Map<string, unknown>;
|
|
5
|
+
/** Session ID for the flow instance. Defaults to crypto.randomUUID(). */
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UseFlowResult<S extends string> {
|
|
9
|
+
/** Current flow state, or null before the flow starts. */
|
|
10
|
+
state: S | null;
|
|
11
|
+
/** Flow context, or null before the flow starts. */
|
|
12
|
+
context: FlowContext | null;
|
|
13
|
+
/** Flow instance ID, or null before the flow starts. */
|
|
14
|
+
flowId: string | null;
|
|
15
|
+
/** Error from the last operation, or null. */
|
|
16
|
+
error: Error | null;
|
|
17
|
+
/** True while startFlow or resume is in progress. */
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
/** Resume the flow with optional external data. */
|
|
20
|
+
resume: (externalData?: Map<string, unknown>) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* React hook that manages a tramli flow lifecycle.
|
|
24
|
+
*
|
|
25
|
+
* Creates a FlowEngine + InMemoryFlowStore once per mount,
|
|
26
|
+
* starts the flow in useEffect, and exposes state/context/resume.
|
|
27
|
+
*/
|
|
28
|
+
export declare function useFlow<S extends string>(definition: FlowDefinition<S>, options?: UseFlowOptions): UseFlowResult<S>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Tramli, InMemoryFlowStore, } from '@unlaxer/tramli';
|
|
3
|
+
/**
|
|
4
|
+
* React hook that manages a tramli flow lifecycle.
|
|
5
|
+
*
|
|
6
|
+
* Creates a FlowEngine + InMemoryFlowStore once per mount,
|
|
7
|
+
* starts the flow in useEffect, and exposes state/context/resume.
|
|
8
|
+
*/
|
|
9
|
+
export function useFlow(definition, options) {
|
|
10
|
+
const [state, setState] = useState(null);
|
|
11
|
+
const [context, setContext] = useState(null);
|
|
12
|
+
const [flowId, setFlowId] = useState(null);
|
|
13
|
+
const [error, setError] = useState(null);
|
|
14
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
15
|
+
// Singleton store + engine per mount
|
|
16
|
+
const storeRef = useRef(null);
|
|
17
|
+
const engineRef = useRef(null);
|
|
18
|
+
const flowIdRef = useRef(null);
|
|
19
|
+
if (storeRef.current === null) {
|
|
20
|
+
storeRef.current = new InMemoryFlowStore();
|
|
21
|
+
engineRef.current = Tramli.engine(storeRef.current);
|
|
22
|
+
}
|
|
23
|
+
// Sync React state from a flow instance
|
|
24
|
+
const syncFromInstance = useCallback((instance) => {
|
|
25
|
+
setState(instance.currentState);
|
|
26
|
+
setContext(instance.context);
|
|
27
|
+
setFlowId(instance.id);
|
|
28
|
+
flowIdRef.current = instance.id;
|
|
29
|
+
}, []);
|
|
30
|
+
// Start flow on mount
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
let cancelled = false;
|
|
33
|
+
async function start() {
|
|
34
|
+
try {
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
setError(null);
|
|
37
|
+
const sessionId = options?.sessionId ?? crypto.randomUUID();
|
|
38
|
+
const initialData = options?.initialData ?? new Map();
|
|
39
|
+
const instance = await engineRef.current.startFlow(definition, sessionId, initialData);
|
|
40
|
+
if (!cancelled) {
|
|
41
|
+
syncFromInstance(instance);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (!cancelled) {
|
|
46
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (!cancelled) {
|
|
51
|
+
setIsLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
start();
|
|
56
|
+
return () => {
|
|
57
|
+
cancelled = true;
|
|
58
|
+
};
|
|
59
|
+
// definition identity is stable per mount — callers should useMemo if needed
|
|
60
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
61
|
+
}, [definition]);
|
|
62
|
+
// Stable resume callback
|
|
63
|
+
const resume = useCallback(async (externalData) => {
|
|
64
|
+
const currentFlowId = flowIdRef.current;
|
|
65
|
+
if (!currentFlowId || !engineRef.current) {
|
|
66
|
+
throw new Error('Flow not started yet — cannot resume');
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
setIsLoading(true);
|
|
70
|
+
setError(null);
|
|
71
|
+
const instance = await engineRef.current.resumeAndExecute(currentFlowId, definition, externalData);
|
|
72
|
+
syncFromInstance(instance);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}, [definition, syncFromInstance]);
|
|
81
|
+
return { state, context, flowId, error, isLoading, resume };
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unlaxer/tramli-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React hooks for tramli — useFlow wraps FlowEngine + FlowStore + FlowInstance lifecycle",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/esm/index.js",
|
|
9
|
+
"require": "./dist/cjs/index.js",
|
|
10
|
+
"types": "./dist/esm/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/cjs/index.js",
|
|
14
|
+
"module": "./dist/esm/index.js",
|
|
15
|
+
"types": "./dist/esm/index.d.ts",
|
|
16
|
+
"files": ["dist"],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc && tsc -p tsconfig.cjs.json",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/opaopa6969/tramli-ts"
|
|
28
|
+
},
|
|
29
|
+
"author": "Hisayuki Ookubo <opaopa6969@gmail.com>",
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=18",
|
|
32
|
+
"@unlaxer/tramli": ">=3.3.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@unlaxer/tramli": "^3.3.0",
|
|
36
|
+
"@types/react": "^18.0.0",
|
|
37
|
+
"react": "^18.0.0",
|
|
38
|
+
"typescript": "^5.5.0"
|
|
39
|
+
}
|
|
40
|
+
}
|