neighbour-hoods / nh-launcher

Creating a group coherence with holochain apps
Other
4 stars 3 forks source link

New applet API #97

Closed adaburrows closed 6 months ago

adaburrows commented 11 months ago

New Applet Interface

Here's a simple skeleton of an example.

Resource renderers and full blocks need to have a specific access to their resources' cells. This means that when they are created and shown they should be given access to their cell directly. The profile store seems like a decent read-only delegate, so we can keep it. The Sensemaker store however may need some thinking about going forward, but for now since apps are responsible for creating contexts until our next release, we can keep this as is. However, as time goes on, we should create affordances to create restricted delegates/proxies to the Sensemaker store.


/**
 * An unsubscribe function returned from a subscribe call
 */
export type UnsubscribeFn = () => void;

/**
 * A callback function to be called when updates occur
 */
export type CallbackFn = (_:Assessment) => void;

/**
 * The only interface needed for a resource renderer.
 * Technically, if we knew what zome call correpsonded to which reading of a
 * resource, we could create a deletgate that only allowed minimal access.
 */
export interface ResourceBlockDelegate {
  appAgentWebsocket: AppAgentClient
}

/**
 * The interface currently exposed to an application for full screen app blocks
 */
export interface AppBlockDelegate {
  appAgentWebsocket: AppAgentClient
  appletInfo: AppletInfo[]
  sensemakerStore: SensemakerStore
  profileStore: ProfilesStore
}

/**
 * The minimal interface needed for an assessment widget
 */
export interface InputAssessmentWidgetDelegate {
  getLatestAssessmentForUser(): Assessment // get the latest assessment value the user created (or none if never assessed or invalidated)
  subscribe(_:CallbackFn): UnsubscribeFn // subscribe to when the current assessment changes
  createAssessment(assessment: Assessment): Assessment // create an assessment
  invalidateLastAssessment(): void // invalidate the last created assessment [ignore for now, we only support creating new assessments]
}

/**
 * The minimal interface needed to render computed assessments
 */
export interface OutputAssessmentWidgetDelegate {
  getLatestAssessment(): Assessment // get the latest computed assessment value (regardless of user, for computed dimensions)
  subscribe(_:CallbackFn): UnsubscribeFn // subscribe to when the current computed dimension changes
}

The OutputAssessmentDisplayDelegate could in theory be a Svelte store, since that implements almost the same interface. I think wrapping a derived Svelte store may be the most flexible way of implementing this. However, if we don't need to allow custom assessment display widgets, this is unnecessary.

The delegate objects will be a plain object conforming to the above interfaces with each method being a closure created in a privileged scope with access to the whole sensemaker. Each specific function will be created to only work with the current resource eh and dimension eh. This provides a cleaner interface for apps instead of emitting an event with the value to be written. It is also slightly more secure, because it means only values that could be chosen by clicking on a button in the UI will be written to the sensemaker instead of any value that could be emitted by a child element.

The existing interface needs to be revised so that the dimensions and methods are decoupled from the component itself. We also need to take into consideration the range of the possible computations given a particular input range (for integers, assume INF = MAXINT):

This means that we can't have pairs that operate on a given set of dimensions unless the computation between input and output is AVG. Otherwise the output range is divergent compared to the input range.

/**
 * Generic constructor type
 */
export type Constructor<T = Object> = new (...args: any[]) => T;

/**
 * Simple interface for allowing a a delegate to be set
 */
export interface NHDelegateReceiver<D> {
  set nhDelegate(delegate: D | null)
}
export type NHDelegateReceiverComponent<D> = HTMLElement & NHDelegateReceiver<D>

/**
 * Defines an Input Assessment Widget
 */
export type InputAssessmentWidgetDefinition = {
  name: string,         // Likely appended to the App name in the dashboard configuration screen
  range: Range,         // Output components must support a range of [-INF, INF] unless it is used with an AVG.
  component: Constructor<NHDelegateReceiverComponent<InputAssessmentWidgetDelegate>>, // Intersection of HTML Element and the delegate interface for 
  kind: 'input'
}

/**
 * Defines an Output Assessment Widget
 */
export type OutputAssessmentWidgetDefinition = {
  name: string,         // Likely appended to the App name in the dashboard configuration screen
  range: Range,         // Output components must support a range of [-INF, INF] unless it is used with an AVG.
  component: Constructor<NHDelegateReceiverComponent<OutputAssessmentWidgetDelegate>>,
  kind: 'output'
}

export type AssessmentWidgetDefinition = InputAssessmentWidgetDefinition | OutputAssessmentWidgetDefinition

/**
 * The new Applet Interface
 */
export interface NeighbourhoodApplet {
  appletConfig: AppletConfigInput;
  appletRenderers: Record<string, Constructor<NHDelegateReceiverComponent<AppBlockDelegate>>>;
  resourceRenderers: Record<string, Constructor<NHDelegateReceiverComponent<ResourceBlockDelegate>>>;
  assessmentWidgets: Record<string, AssessmentWidgetDefinition>;
}

Example:

const feedApplet: NeighbourhoodApplet = {
  appletConfig: {
    "name": "todo_applet",
    "resource_defs": [{
      "role_name": "todo_lists",
      "zome_name": "todo",
      "name": "task_item",
      "base_types": [{ "entry_index": 0, "zome_index": 0, "visibility": { "Public": null } }],
    }],
    // This is going to be changed in the next release, or whenever we figure out how contexts are configured by the CA
    "cultural_contexts": [mostImportantTasksContext, hottestTasksContext]
  },
  appletRenderers: {
    "full": CustomApp
  },
  resourceRenderers: {
    "task_item": ResourceRenderer
  },
  assessmentWidgets: {
    "importance": {
      name: "Importance Ranker",
      range: {
        min: 0,
        max: 10
      }
      component: ImportanceWidget,
      kind: "input"
    }
  }
}

Also, since the configuration could be completely assigned from scratch by the CA, the required fields on the applet config will be reduced:


export interface AppletConfigInput {
    name: string,
    resource_defs: Array<ConfigResourceDef>,
    cultural_contexts: Array<ConfigCulturalContext>,
    // These become suggestions rather than enforced things.
    methods?: Array<ConfigMethod>,
    ranges?: Array<Range>,
    dimensions?: Array<ConfigDimension>,
}

const appletConfig: AppletConfigInput = {
    "name": "todo_applet",
    "resource_defs": { "todo_lists": { "todo": [taskItemResourceDef] } },
    // This is going to be changed in the next release, or whenever we figure out how contexts are configured by the CA
    "cultural_contexts": [mostImportantTasksContext, hottestTasksContext]
}
adaburrows commented 10 months ago

Due to the other work towards #97 & #102, we now have the supporting types and new block-renderer and derived components.

Once https://github.com/neighbour-hoods/sensemaker-lite/issues/73 is done along with #109, #110, #111: