Depending on Plugins

Standing on the Shoulders of Giants

While it is useful for The Simplest Plugin to add its own isolated logic to a LearnCard, part of the beauty of LearnCard plugins is to depend on other plugins 💪

Plugin dependence comes in two flavors:

Boilerplate Plugins

To demonstrate this, let's create a simple base plugin, as well as two plugins that we will depend on: one for Control Planes, and one for methods.

Base Plugin

src/dependence/types.ts
import { Plugin } from '@learncard/core';

export type DependencePluginType = Plugin<'Dependence', any, { bar: () => 'baz' }>;
src/dependence/index.ts
import { DependencePluginType } from './types';

export const DependencePlugin: DependencePluginType = {
    name: 'Dependence',
    methods: { bar: () => 'baz' },
};

Control Plane Plugin

src/controlplane/types.ts
import { Plugin } from '@learncard/core';

export type ControlPlanePluginType = Plugin<'Control Plane', 'id'>;
src/controlplane/index.ts
import { ControlPlanePluginType } from './types';

export const ControlPlanePlugin: ControlPlanePluginType = {
    name: 'Control Plane',
    id: {
        did: () => { throw new Error('TODO'); },
        keypair: () => { throw new Error('TODO'); },
    },
    methods: {},
}

Methods Plugin

src/methods/types.ts
import { Plugin } from '@learncard/core';

export type MethodsPluginMethods = {
    foo: () => 'bar';
};

export type MethodsPluginType = Plugin<'Methods', any, MethodsPluginMethods>;
src/methods/index.ts
import { MethodsPluginType } from './types';

export const MethodsPlugin: MethodsPluginType = {
    name: 'Methods',
    methods: { foo: () => 'bar' },
}

Dependence Convention

As a convention, plugins will often be wrapped inside of a constructor function that requires a LearnCard of a certain type be passed in.

Let's update our base plugin to see what that looks like:

src/dependence/types.ts
import { Plugin } from '@learncard/core';

export type DependencePlugin = Plugin<'Dependence', any, { bar: () => 'baz' }>;
src/dependence/index.ts
import { LearnCard } from '@learncard/core';
import { DependencePlugin } from './types';

export const getDependencePlugin: (learnCard: LearnCard<any>): DependencePluginType => ({
    name: 'Dependence',
    methods: { bar: () => 'baz' },
});

With this change, LearnCards that would like to add our plugin will now look slightly different:

// Old
const withPlugin = await learnCard.addPlugin(DependencePlugin);

// New
const withPlugin = await learnCard.addPlugin(getDependencePlugin(learnCard));

Adding dependency requirements

Depending on Planes

Now that we're requiring a LearnCard be passed in, we are able to add requirements to the dependent LearnCard! Let's take a look at what that looks like by attempting to take a dependency on the Control Plane Plugin.

Note: The LearnCard SDK does not support depending on literal plugins. It only supports depending on what plugins implement. In practice, this makes plugins much more flexible and easy to work with, allowing dependent plugins to be hot-swapped easily as long as they implement the same dependent methods/planes.

src/dependence/index.ts
import { LearnCard } from '@learncard/core';
import { DependencePlugin } from './types';

export const getDependencePlugin: (learnCard: LearnCard<any, 'id'>): DependencePluginType => {
    console.log('Successfully depended on a Control Plane!', learnCard.id.did());
    
    return {
        name: 'Dependence',
        methods: { bar: () => 'baz' },
    };
};

The operative change is right here on line 4:

//                                                           VVVV
export const getDependencePlugin: (learnCard: LearnCard<any, 'id'>): DependencePluginType => {
//                                                           ^^^^

This change allows us to call learnCard.id.did on line 5, and requires consumers of this plugin to pass in a LearnCard that implements the ID plane when adding this plugin. For example, all of the following code will throw errors:

import { initLearnCard } from '@learncard/core';

const learnCard = await initLearnCard({ custom: true });

const errors = await learnCard.addPlugin(getDependencePlugin(learnCard));
// TS Error: Property 'id' is missing

Depending on Methods

Depending on a specific method (or methods) rather than a Control Plane looks very similar. To demonstrate this, let's stop depending on the ID Plane for a moment, and instead just depend on the foo method from the Methods Plugin.

src/dependence/index.ts
import { LearnCard } from '@learncard/core';
import { DependencePlugin } from './types';

export const getDependencePlugin: (learnCard: LearnCard<any, any, { foo: () => 'bar' }>): DependencePluginType => {
    console.log('Successfully depended on a Method!', learnCard.invoke.foo());
    
    return {
        name: 'Dependence',
        methods: { bar: () => 'baz' },
    };
};

The operative change is, once again, on line 4:

//                                                                VVVVVVVVVVVVVVVVVVVV
export const getDependencePlugin: (learnCard: LearnCard<any, any, { foo: () => 'bar' }>): DependencePluginType => {
//                                                                ^^^^^^^^^^^^^^^^^^^^

With this change in place, just like when we depended on a Control Plane, we are now able to call learnCard.invoke.foo on line 5. We also now require consumers of this plugin to pass in a LearnCard with a plugin that implements the foo method. For example, all of the following code will throw errors:

import { initLearnCard } from '@learncard/core';

const learnCard = await initLearnCard({ custom: true });

const errors = await learnCard.addPlugin(getDependencePlugin(learnCard));
// TS Error: Property 'foo' is missing

Adding Type Safety to The Implicit LearnCard

Thus far, when adding dependency requirements, we have only added type safety to the argument of our constructor function. This works quite well, but does not provide type safety to the Implicit LearnCard passed into every method. To add this type safety, we use the fourth and fifth generic arguments of the Plugin type

Let's demonstrate this by first depending on both the Control Plane Plugin and the Methods Plugin, then adding some logic that uses the Implicit LearnCard to take advantage of that dependency:

src/dependence/index.ts
import { LearnCard } from '@learncard/core';
import { DependencePlugin } from './types';

export const getDependencePlugin: (learnCard: LearnCard<any, 'id', { foo: () => 'bar' }>): DependencePluginType => {
    return {
        name: 'Dependence',
        methods: { 
            bar: _learnCard => {
                 // these two calls with throw TS errors!
                 console.log('Did is:', _learnCard.id.did());
                 console.log('Foo is:', _learnCard.invoke.foo());
            
                return 'baz';
            } 
        },
    };
};

Because we haven't added our dependencies to the DependencePlugin type itself, TS has no way of knowing that the Implicit LearnCard implements the ID Plane and the foo method! We can easily fix this by updating the DependencePlugin type:

src/dependence/types.ts
import { Plugin } from '@learncard/core';

//                                                                             VVVVVVVVVVVVVVVVVVVVVVVVVV
export type DependencePlugin = Plugin<'Dependence', any, { bar: () => 'baz' }, 'id', { foo: () => 'bar' }>;
//                                                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^

With these in place, TS will know to add the ID Plane and the foo method to the Implicit LearnCard, and the above errors will go away!

Last updated