From bc38e47d5dc56e6b879618215c051c9b7a6e318c Mon Sep 17 00:00:00 2001 From: wangyi Date: Tue, 12 Apr 2022 09:49:44 +0800 Subject: [PATCH] # 4.3.1 2022-04-12 * [design] add [avatar](/experience?id=avatar-experience-1) API. --- docs/changes.md | 6 +- docs/experience.md | 235 ++++++++++++++++++++++++++++++++- docs/zh/changes.md | 6 +- docs/zh/experience.md | 234 +++++++++++++++++++++++++++++++- index.d.ts | 9 ++ package.json | 2 +- src/index.ts | 4 +- src/libs/avatar.ts | 31 +++++ src/libs/connector.ts | 9 +- src/libs/defines.ts | 2 + src/libs/effect.ts | 8 +- src/libs/global.type.ts | 10 +- src/libs/reducer.ts | 10 +- test/experience/avatar.test.ts | 179 +++++++++++++++++++++++++ 14 files changed, 729 insertions(+), 16 deletions(-) create mode 100644 src/libs/avatar.ts create mode 100644 test/experience/avatar.test.ts diff --git a/docs/changes.md b/docs/changes.md index 51b510f..52194ff 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -180,4 +180,8 @@ in this version, `runtime.cache` used in MiddleWare is independent. # 4.3.0 2022-04-06 * [design] add [flow](/experience?id=flow-experience) and [effect](/experience?id=effect-decorator-experience). -* [design] add experience, set `process.env.AGENT_REDUCER_EXPERIENCE` to `OPEN`, can use the experience features. \ No newline at end of file +* [design] add experience, set `process.env.AGENT_REDUCER_EXPERIENCE` to `OPEN`, can use the experience features. + +# 4.3.1 2022-04-12 + +* [design] add [avatar](/experience?id=avatar-experience-1) API. \ No newline at end of file diff --git a/docs/experience.md b/docs/experience.md index f397c9c..8916248 100644 --- a/docs/experience.md +++ b/docs/experience.md @@ -365,6 +365,217 @@ describe('subscribe error',()=>{ ``` +### avatar (experience) + +Flow can do many thing to compose requests and state change methods together as a work flow. Sometimes, we even want to use the interface functions from platform, but, use these interfaces directly in model is not a good idea. That make model difficult to move to other platforms. + +Now, we add a new API `avatar` to resolve this problem. `Avatar` use the `Algebraic Effects` mode to fix it. You can describe a simple `interfaces object` to replace the functions from platform, and implements these functions before using in the platform codes. + +When the method of `avatar(interfaces).current` is used, it always finds the if the function is implemented, and use the implement one as possible as it can, if the function is not exist in the implement one, it use the relative function from `interfaces object` for a replace. + +How to use a global avatar? + +```typescript +import { + Flows, + flow, + create, + effect, + experience, + avatar, + Model +} from "agent-reducer"; + +describe('how to use global avatar', () => { + + type User = { + id: number, + name: string + }; + + type UserListState = { + source: User[] | null, + loading: boolean, + } + + const dataSource: User[] = [ + {id: 1, name: 'Jimmy'}, + {id: 2, name: 'Jacky'}, + {id: 3, name: 'Lucy'}, + {id: 4, name: 'Lily'}, + {id: 5, name: 'Nike'}, + ]; + + const prompt = avatar({ + success:(info:string)=>undefined, + error:(e:any)=>undefined + }); + + class UserListModel implements Model { + + state: UserListState = { + source: [], + loading: false, + }; + + private load() { + return {...this.state, loading: true}; + } + + private changeSource(source: User[] | null) { + return {...this.state, source}; + } + + private unload() { + return {...this.state, loading: false}; + } + + @flow(Flows.latest()) + async loadSource() { + this.load(); + try { + const source: User[] = await new Promise((resolve) => { + resolve([...dataSource]); + }); + this.changeSource(source); + // use prompt.current.success to popup a message `fetch success` + prompt.current.success('fetch success!'); + } catch (e) { + // use prompt.current.error to popup a error message + prompt.current.error(e); + }finally { + this.unload(); + } + } + + } + + test('if you want to call outside effect function in model, you can use API `avatar`', async () => { + const success = jest.fn().mockImplementation((info:string)=>console.log(info)); + // implement the interfaces of prompt avatar + const destroy = prompt.implement({ + success, + }); + const {agent, connect, disconnect} = create(UserListModel); + connect(); + await agent.loadSource(); + expect(success).toBeCalledTimes(1); + disconnect(); + // if you do not need this avatar, + // please destroy it finally + destroy(); + }); + +}); +``` + +How to use avatar for different model instances. + +```typescript +import { + Flows, + flow, + create, + effect, + experience, + avatar, + Model +} from "agent-reducer"; + +describe('how to use model avatar', () => { + + type User = { + id: number, + name: string + }; + + type UserListState = { + source: User[] | null, + list: User[], + filterName: string, + loading: boolean, + } + + const dataSource: User[] = [ + {id: 1, name: 'Jimmy'}, + {id: 2, name: 'Jacky'}, + {id: 3, name: 'Lucy'}, + {id: 4, name: 'Lily'}, + {id: 5, name: 'Nike'}, + ]; + + class UserListModel implements Model { + + state: UserListState = { + source: [], + list: [], + filterName: '', + loading: false, + }; + + prompt = avatar({ + success:(info:string)=>undefined, + error:(e:any)=>undefined + }); + + private load() { + return {...this.state, loading: true}; + } + + private changeSource(source: User[] | null) { + return {...this.state,source} + } + + private unload() { + return {...this.state, loading: false}; + } + + @flow(Flows.latest()) + async loadSource() { + this.load(); + try { + const source: User[] = await new Promise((resolve) => { + resolve([...dataSource]); + }); + this.changeSource(source); + // use prompt.current.success to popup a message `fetch success` + this.prompt.current.success('fetch success!'); + } catch (e) { + // use prompt.current.error to popup a error message + this.prompt.current.error(e); + }finally { + this.unload(); + } + } + + } + + test('If you want to use `avatar` in model, please build avatar inside model', async () => { + const success = jest.fn().mockImplementation((info:string)=>console.log(info)); + + const {agent, connect, disconnect} = create(UserListModel); + const {agent:another, connect:anotherConnect, disconnect:anotherDisconnect} = create(UserListModel); + // implement avatar for different models + const destroy = agent.prompt.implement({ + success, + }); + connect(); + anotherConnect(); + await agent.loadSource(); + await another.loadSource(); + // the agent.prompt is implemented with avatar, + // the another one is not. + expect(success).toBeCalledTimes(1); + disconnect(); + anotherDisconnect(); + // if you do not need this avatar, + // please destroy it finally + destroy(); + }); + +}); +``` + ### Effect decorator (experience) If you want to add effect inside model, and start it after this model is connected, you can use api [effect](/experience?id=effect-experience) to decorate a model method to be a effect callback. If you pass `*` into [effect](/experience?id=effect-experience) decorator, it will take all the methods of current model instance as the listening target. If you pass a callback which returns a method of current model into [effect](/experience?id=effect-experience) decorator as a param, it will only listen to the state changes leaded by this specific `method`. @@ -746,4 +957,26 @@ export declare function effect=Model>( ):MethodDecoratorCaller ``` -* method - optional, a callback which returns `Class.prototype.method` as the target method for state change listening. \ No newline at end of file +* method - optional, a callback which returns `Class.prototype.method` as the target method for state change listening. + +### avatar (experience) + +Create an avatar object for outside interfaces. + +```typescript +export type Avatar> = { + current:T, + implement:(impl:Partial)=>()=>void; +}; + +export declare function avatar< + T extends Record + >(interfaces:T):Avatar; +``` + +* interfaces - provide a default simulate `interfaces object`, if the used function from implements is not exist, it will provide the right function for a replace. + +return Avatar object: + +* current - the current working interfaces object. If the used function from implements is not exist, it will take the right function from `interfaces object` for a replace. +* implement - callback, accept a implement object for `interfaces object`. \ No newline at end of file diff --git a/docs/zh/changes.md b/docs/zh/changes.md index 05d0099..8829342 100644 --- a/docs/zh/changes.md +++ b/docs/zh/changes.md @@ -188,4 +188,8 @@ # 4.3.0 2022-04-06 * [design] 添加 [flow](/zh/experience?id=工作流-体验) 和 [effect decorator](/zh/experience?id=副作用-decorator-装饰器用法-体验). -* [design] 添加 experience 体验模式,设置 `process.env.AGENT_REDUCER_EXPERIENCE` 为 `OPEN`, 可体验最新特性和 API. \ No newline at end of file +* [design] 添加 experience 体验模式,设置 `process.env.AGENT_REDUCER_EXPERIENCE` 为 `OPEN`, 可体验最新特性和 API. + +# 4.3.1 2022-04-12 + +* [design] 添加 [avatar](/zh/experience?id=avatar-体验) API. \ No newline at end of file diff --git a/docs/zh/experience.md b/docs/zh/experience.md index 717ac6c..a4d05ee 100644 --- a/docs/zh/experience.md +++ b/docs/zh/experience.md @@ -261,6 +261,214 @@ describe('how to use flow', () => { }); ``` +### 替身 (体验) + +在使用模型工作流的过程中,我们常常还需要加入 UI 调用,中断等待数据处理等步骤来完善任务。如果直接调用 UI 函数,以及部分平台相关的外部数据等待函数,那么我们的模型将被当前平台所束缚,失去了该有的易迁移特性。 + +至 `agent-reducer@4.3.1` ,我们在体验版中新增了 `avatar` API,用于代替 UI 及平台相关操作,从而降低模型对平台的依赖性。 + +`avatar` 替身是一种对 `代数效应 (Algebraic Effects)` 设计模式的实现。我们通过预设一系列只通过入参直接返回默认值的函数来做替身,并在模型中直接使用替身函数来做临时工作,然后在外部平台层,通过 `avatar(interface).implement(impl)` 来实现当前替身在平台运行环境中的代码。在模型 flow 工作流调用的时候,`avatar(interface).current` 会根据已实现的 `impl` 和替身 `interface` 来决定该用哪一个来工作。如被使用接口在 `impl` 中存在,则会直接使用,否则用替身 `interface` 中对应的接口来做弥补。 + +简单实用 avatar : + +```typescript +import { + Flows, + flow, + create, + effect, + experience, + avatar, + Model +} from "agent-reducer"; + +describe('how to use global avatar', () => { + + type User = { + id: number, + name: string + }; + + type UserListState = { + source: User[] | null, + loading: boolean, + } + + const dataSource: User[] = [ + {id: 1, name: 'Jimmy'}, + {id: 2, name: 'Jacky'}, + {id: 3, name: 'Lucy'}, + {id: 4, name: 'Lily'}, + {id: 5, name: 'Nike'}, + ]; + + const prompt = avatar({ + success:(info:string)=>undefined, + error:(e:any)=>undefined + }); + + class UserListModel implements Model { + + state: UserListState = { + source: [], + loading: false, + }; + + private load() { + return {...this.state, loading: true}; + } + + private changeSource(source: User[] | null) { + return {...this.state, source}; + } + + private unload() { + return {...this.state, loading: false}; + } + + @flow(Flows.latest()) + async loadSource() { + this.load(); + try { + const source: User[] = await new Promise((resolve) => { + resolve([...dataSource]); + }); + this.changeSource(source); + // 弹出 `fetch success` 提示信息框 + prompt.current.success('fetch success!'); + } catch (e) { + // 弹出错误提示框 + prompt.current.error(e); + }finally { + this.unload(); + } + } + + } + + test('如果希望在工作流中调用平台相关效果API,可使用 API `avatar`', async () => { + const success = jest.fn().mockImplementation((info:string)=>console.log(info)); + // 实现替身接口函数 + const destroy] = prompt.implement({ + success, + }); + const {agent, connect, disconnect} = create(UserListModel); + connect(); + await agent.loadSource(); + expect(success).toBeCalledTimes(1); + disconnect(); + // 在不再需要替身时,可以销毁它 + destroy(); + }); + +}); +``` + +如果希望在按模型实例的不通调用不同的实现,可以将替身挂在模型里,并对不同的模型实例提供的替身进行实现。 + +```typescript +import { + Flows, + flow, + create, + effect, + experience, + avatar, + Model +} from "agent-reducer"; + +describe('how to use model avatar', () => { + + type User = { + id: number, + name: string + }; + + type UserListState = { + source: User[] | null, + list: User[], + filterName: string, + loading: boolean, + } + + const dataSource: User[] = [ + {id: 1, name: 'Jimmy'}, + {id: 2, name: 'Jacky'}, + {id: 3, name: 'Lucy'}, + {id: 4, name: 'Lily'}, + {id: 5, name: 'Nike'}, + ]; + + class UserListModel implements Model { + + state: UserListState = { + source: [], + list: [], + filterName: '', + loading: false, + }; + + prompt = avatar({ + success:(info:string)=>undefined, + error:(e:any)=>undefined + }); + + private load() { + return {...this.state, loading: true}; + } + + private changeSource(source: User[] | null) { + return {...this.state,source} + } + + private unload() { + return {...this.state, loading: false}; + } + + @flow(Flows.latest()) + async loadSource() { + this.load(); + try { + const source: User[] = await new Promise((resolve) => { + resolve([...dataSource]); + }); + this.changeSource(source); + // 弹出 `fetch success` 提示信息框 + this.prompt.current.success('fetch success!'); + } catch (e) { + // 弹出错误提示信息框 + this.prompt.current.error(e); + }finally { + this.unload(); + } + } + + } + + test('If you want to use `avatar` in model, please build avatar inside model', async () => { + const success = jest.fn().mockImplementation((info:string)=>console.log(info)); + + const {agent, connect, disconnect} = create(UserListModel); + const {agent:another, connect:anotherConnect, disconnect:anotherDisconnect} = create(UserListModel); + // 为不同的模型实例实现替身接口 + const destroy = agent.prompt.implement({ + success, + }); + connect(); + anotherConnect(); + await agent.loadSource(); + await another.loadSource(); + // agent.prompt 被实现了,但 another 没有 + expect(success).toBeCalledTimes(1); + disconnect(); + anotherDisconnect(); + // 如果不再使用可以销毁掉 + destroy(); + }); + +}); +``` + ### 副作用 decorator 装饰器用法 (体验) 添加副作用的 decorator API 为 [effect](/zh/experience?id=effect-体验)。被该 decorator 函数修饰的方法将被作为副作用回调来使用,而副作用监听目标为该函数入参,如:`effect('*')` 表示监听所有 state 变化,如传入当前模型方法提供函数,则监听该目标下的指定方法 `effect(()=>Model.prototype.method)`。 @@ -529,4 +737,28 @@ export declare function effect=Model>( * method - 可选,返回被监听的目标方法的回调函数,必须为当前模型方法。 -查看更多[细节](/zh/guides?id=副作用-decorator-装饰器用法)。 \ No newline at end of file +查看更多[细节](/zh/guides?id=副作用-decorator-装饰器用法)。 + +### avatar (体验) + +维护一个替身接口对象,在使用时,根据已分配的实现接口运行。若接口已实现,则运行已经实现的接口,否则运行默认接口。 + +```typescript +export type Avatar> = { + current:T, + implement:(impl:Partial)=>()=>void; +}; + +export declare function avatar< + T extends Record + >(interfaces:T):Avatar; +``` + +* interfaces - 默认接口对象。 + +返回 Avatar 对象: + +* current - 默认接口与实现接口合并后的接口集合。 +* implement - 实现方法,传入的 `impl` 对象作为实现接口。 + +该方法主要用作平台接口与模型的交接。 \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 945d160..d315b79 100644 --- a/index.d.ts +++ b/index.d.ts @@ -187,6 +187,15 @@ export declare function subscribeError>( listener:ErrorListener ):(()=>void); +export type Avatar> = { + current:T, + implement:(impl:Partial)=>()=>void; +}; + +export declare function avatar< + T extends Record + >(interfaces:T):Avatar; + export declare function experience():void; export type LaunchHandler = { diff --git a/package.json b/package.json index 2d7c229..40d20d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-reducer", - "version": "4.3.0", + "version": "4.3.1", "main": "dist/agent-reducer.mini.js", "typings": "index.d.ts", "author": "Jimmy.Harding", diff --git a/src/index.ts b/src/index.ts index 0ad9ff0..96a2f69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,10 +30,12 @@ export { export { addEffect, effectDecorator as effect } from './libs/effect'; -/** act * */ +/** flow * */ export { default as flow } from './libs/flow'; export { Flows } from './libs/flows'; +export { default as avatar } from './libs/avatar'; + /** experience * */ export { experience } from './libs/experience'; diff --git a/src/libs/avatar.ts b/src/libs/avatar.ts new file mode 100644 index 0000000..401004f --- /dev/null +++ b/src/libs/avatar.ts @@ -0,0 +1,31 @@ +import { createProxy, isObject, validate } from './util'; +import { Avatar } from './global.type'; +import { validateExperience } from './experience'; + +export default function avatar< + T extends Record + >(interfaces:T):Avatar { + validateExperience(); + validate(isObject(interfaces) || Array.isArray(interfaces), 'you need to provide a object or an array as a `interfaces`'); + let global:Partial|undefined; + return { + current: createProxy(interfaces, { + set(target: T, p: string, value: any, receiver: any): boolean { + return false; + }, + get(target: T, p: string, receiver: any): any { + const avatarObj = global || interfaces; + if (p in avatarObj) { + return avatarObj[p]; + } + return interfaces[p]; + }, + }) as T, + implement(impl:Partial) { + global = impl; + return () => { + global = undefined; + }; + }, + }; +} diff --git a/src/libs/connector.ts b/src/libs/connector.ts index b078395..dc06702 100644 --- a/src/libs/connector.ts +++ b/src/libs/connector.ts @@ -6,7 +6,9 @@ import { agentEffectsKey, agentErrorConnectionKey, agentListenerKey, - agentMethodName, agentModelMethodsCacheKey, + agentMethodName, + agentModelInstanceInitialedKey, + agentModelMethodsCacheKey, agentModelResetKey, agentSharingMiddleWareKey, agentSharingTypeKey, DefaultActionType, @@ -21,6 +23,7 @@ export function resetModel< entity[agentSharingMiddleWareKey] = undefined; entity[agentActionKey] = undefined; entity[agentModelMethodsCacheKey] = undefined; + entity[agentModelInstanceInitialedKey] = undefined; unmountEffects(entity); entity[agentEffectsKey] = undefined; entity[agentErrorConnectionKey] = undefined; @@ -98,7 +101,11 @@ function initialModel< if (!instance[agentModelMethodsCacheKey]) { instance[agentModelMethodsCacheKey] = {}; } + if (instance[agentModelInstanceInitialedKey]) { + return; + } mountMethod(instance); + instance[agentModelInstanceInitialedKey] = true; } function notification< diff --git a/src/libs/defines.ts b/src/libs/defines.ts index 644bf13..8773388 100644 --- a/src/libs/defines.ts +++ b/src/libs/defines.ts @@ -38,6 +38,8 @@ export const agentConnectorKey = '@@agent-connector'; export const agentModelMethodsCacheKey = '@@agent-model-methods-cache'; +export const agentModelInstanceInitialedKey = '@@agent-model-instance-initialed'; + export enum DefaultActionType { DX_INITIAL_STATE = '@@AGENT_REDUCER_INITIAL_STATE', DX_MUTE_STATE = '@@AGENT_MUTE_STATE', diff --git a/src/libs/effect.ts b/src/libs/effect.ts index d5bfaf6..f7aa610 100644 --- a/src/libs/effect.ts +++ b/src/libs/effect.ts @@ -17,7 +17,7 @@ import { agentModelWorking, } from './defines'; import { unblockThrow, validate, warn } from './util'; -import { isConnecting } from './status'; +import { stateUpdatable } from './status'; import { validateExperience } from './experience'; function extractValidateMethodName = Model>( @@ -104,7 +104,7 @@ function createEffect = Model>( } export function runningNotInitialedModelEffects = Model>(model:T):void { - if (!isConnecting(model)) { + if (!stateUpdatable(model)) { return; } const effects = model[agentEffectsKey] || []; @@ -121,7 +121,7 @@ export function addEffect = Model>( model:T, method?:keyof T|ModelInstanceMethod|'*', ):EffectWrap { - validate(isConnecting(model), 'The target model is unconnected'); + validate(stateUpdatable(model), 'The target model instance is expired'); const methodName = extractValidateMethodName(model, method); const effect:Effect = createEffect(callback, model, methodName); @@ -178,7 +178,7 @@ export function runEffects = Model>( unblockThrow(e); } } - if (!isConnecting(model)) { + if (!stateUpdatable(model)) { return; } effectCopies.forEach(runDestroy.bind(null, action)); diff --git a/src/libs/global.type.ts b/src/libs/global.type.ts index 16ab794..72cffaa 100644 --- a/src/libs/global.type.ts +++ b/src/libs/global.type.ts @@ -17,7 +17,9 @@ import { agentConnectorKey, agentMethodActsKey, agentActMethodAgentLaunchHandlerKey, - agentIsEffectAgentKey, agentModelMethodsCacheKey, + agentIsEffectAgentKey, + agentModelMethodsCacheKey, + agentModelInstanceInitialedKey, } from './defines'; export type SharingType = 'hard'|'weak'; @@ -112,6 +114,7 @@ export interface OriginAgent { [agentActMethodAgentLaunchHandlerKey]?:LaunchHandler, [agentIsEffectAgentKey]?:boolean, [agentModelMethodsCacheKey]?:Record; + [agentModelInstanceInitialedKey]?:boolean, [agentConnectorKey]?:Connector, } @@ -192,3 +195,8 @@ export type Connection = { } export type ConnectionFactory> = ((entity:T)=>Connection); + +export type Avatar> = { + current:T, + implement:(impl:Partial)=>()=>void; +}; diff --git a/src/libs/reducer.ts b/src/libs/reducer.ts index b7a01cd..61b43f7 100644 --- a/src/libs/reducer.ts +++ b/src/libs/reducer.ts @@ -97,14 +97,17 @@ function extractAction>(entity:T):Action|null { return null; } -function linkAction>(entity:T, action:Action):ActionWrap { +function linkAction>(entity:T, action:Action):void { const actionWrap = entity[agentActionKey]; + if (!stateUpdatable(entity)) { + return; + } const wrap:ActionWrap = { current: action, }; if (!actionWrap) { entity[agentActionKey] = wrap; - return wrap; + return; } const { last } = actionWrap; if (!last) { @@ -114,12 +117,11 @@ function linkAction>(entity:T, action:Action):ActionWrap { last.next = wrap; actionWrap.last = wrap; } - return actionWrap; } function shiftAction>(entity:T):Action|null { const actionWrap = entity[agentActionKey]; - if (!actionWrap) { + if (!actionWrap || !stateUpdatable(entity)) { return null; } const { last, next } = actionWrap; diff --git a/test/experience/avatar.test.ts b/test/experience/avatar.test.ts new file mode 100644 index 0000000..8feb72f --- /dev/null +++ b/test/experience/avatar.test.ts @@ -0,0 +1,179 @@ +import {Flows, flow, create, effect, experience, avatar} from "../../src"; +import {Model} from '../../index'; + +experience(); + +describe('how to use global avatar', () => { + + type User = { + id: number, + name: string + }; + + type UserListState = { + source: User[] | null, + loading: boolean, + } + + const dataSource: User[] = [ + {id: 1, name: 'Jimmy'}, + {id: 2, name: 'Jacky'}, + {id: 3, name: 'Lucy'}, + {id: 4, name: 'Lily'}, + {id: 5, name: 'Nike'}, + ]; + + const prompt = avatar({ + success:(info:string)=>undefined, + error:(e:any)=>undefined + }); + + class UserListModel implements Model { + + state: UserListState = { + source: [], + loading: false, + }; + + private load() { + return {...this.state, loading: true}; + } + + private changeSource(source: User[] | null) { + return {...this.state, source}; + } + + private unload() { + return {...this.state, loading: false}; + } + + @flow(Flows.latest()) + async loadSource() { + this.load(); + try { + const source: User[] = await new Promise((resolve) => { + resolve([...dataSource]); + }); + this.changeSource(source); + // use prompt.current.success to popup a message `fetch success` + prompt.current.success('fetch success!'); + } catch (e) { + // use prompt.current.error to popup a error message + prompt.current.error(e); + }finally { + this.unload(); + } + } + + } + + test('if you want to call outside effect function in model, you can use API `avatar`', async () => { + const success = jest.fn().mockImplementation((info:string)=>console.log(info)); + // implement the interfaces of prompt avatar + const destroy = prompt.implement({ + success, + }); + const {agent, connect, disconnect} = create(UserListModel); + connect(); + await agent.loadSource(); + expect(success).toBeCalledTimes(1); + disconnect(); + // if you do not need this avatar, + // please destroy it finally + destroy(); + }); + +}); + +describe('how to use model avatar', () => { + + type User = { + id: number, + name: string + }; + + type UserListState = { + source: User[] | null, + list: User[], + filterName: string, + loading: boolean, + } + + const dataSource: User[] = [ + {id: 1, name: 'Jimmy'}, + {id: 2, name: 'Jacky'}, + {id: 3, name: 'Lucy'}, + {id: 4, name: 'Lily'}, + {id: 5, name: 'Nike'}, + ]; + + class UserListModel implements Model { + + state: UserListState = { + source: [], + list: [], + filterName: '', + loading: false, + }; + + prompt = avatar({ + success:(info:string)=>undefined, + error:(e:any)=>undefined + }); + + private load() { + return {...this.state, loading: true}; + } + + private changeSource(source: User[] | null) { + return {...this.state,source} + } + + private unload() { + return {...this.state, loading: false}; + } + + @flow(Flows.latest()) + async loadSource() { + this.load(); + try { + const source: User[] = await new Promise((resolve) => { + resolve([...dataSource]); + }); + this.changeSource(source); + // use prompt.current.success to popup a message `fetch success` + this.prompt.current.success('fetch success!'); + } catch (e) { + // use prompt.current.error to popup a error message + this.prompt.current.error(e); + }finally { + this.unload(); + } + } + + } + + test('If you want to use `avatar` in model, please build avatar inside model', async () => { + const success = jest.fn().mockImplementation((info:string)=>console.log(info)); + + const {agent, connect, disconnect} = create(UserListModel); + const {agent:another, connect:anotherConnect, disconnect:anotherDisconnect} = create(UserListModel); + // implement avatar for different models + const destroy = agent.prompt.implement({ + success, + }); + connect(); + anotherConnect(); + await agent.loadSource(); + await another.loadSource(); + // the agent.prompt is implemented with avatar, + // the another one is not. + expect(success).toBeCalledTimes(1); + disconnect(); + anotherDisconnect(); + // if you do not need this avatar, + // please destroy it finally + destroy(); + }); + +});