Documentation
Service

Service

Service is a special abstract class that is used to create custom actions, behaviors, or any other custom logic that can be shared across multiple scenes.

To use it, you need to extend and fully implement the abstract methods.

import {Service} from "narraleaf-react";

Example

Here is an example of a custom service that implements a simple gallery service.

type GalleryActions = {
    "add": [name: string]
};
 
class Gallery extends Service<GalleryActions> {
    // custom data
    unlocked: string[] = [];
 
    constructor() {
        super();
 
        // register the action handler
        this.onAction("add", (ctx: ServiceHandlerCtx, name: string) => {
            console.log("Adding", name);
            this.unlocked.push(name);
        })
    }
 
    /* Implement the serialize and deserialize methods */
    serialize(): Record<string, any> | null {
        return {
            unlocked: this.unlocked
        };
    }
    deserialize(data: Record<string, any>): void {
        this.unlocked = data.unlocked;
    }
 
    /* Custom service logic */
    add(name: string) {
        // trigger the action
        return this.action("add", name);
    }
}

and we can use this service in the game:

const gallery = new Gallery();
 
myScene.action([
    gallery
        .add("image1")
        .add("image2")
        .add("image3"),
]);
 

Creating Custom Service

Implementing Service

To implement a service, you need to extend the Service class and implement the abstract methods.

class MyCustomService extends Service {
    /**
     * Serialize the service to data
     *
     * **Note**: data must be JSON serializable, return null if nothing needs to be saved
     */
    serialize(): Record<string, any> | null {
        return null;
    }
 
    /**
     * Load data to the service
     * @param data data exported from toData
     */
    deserialize(data: Record<string, any>): void {
    }
}

Registering Actions

To register actions, you need to use the onAction method.
All registration should be done in the constructor.

class MyCustomService extends Service {
    constructor() {
        super();
 
        this.onAction("myAction", (ctx: ServiceHandlerCtx, ...args: any[]) => {
            // custom logic
        });
    }
}

For ServiceHandlerCtx, see ServiceHandlerCtx.

To enable type checking, you can define the action types.

type MyCustomActions = {
    "myAction": [arg0: string, arg1: number]
};
 
class MyCustomService extends Service<MyCustomActions> {
    constructor() {
        super();
 
        this.onAction("myAction", (ctx: ServiceHandlerCtx, arg0: string, arg1: number) => {
            // custom logic
        });
    }
}

Async Actions

If you need to perform async operations, you can return a promise.
But for future use, you should make the action abortable.

this.onAction("myAction", (ctx: ServiceHandlerCtx, ...args: any[]) => {
    const abortController = new AbortController();
    const { signal } = abortController;
    const promise = fetch("https://example.com", { signal }); // some async operation
 
    ctx.onAbort(() => {
        abortController.abort(); // abort the async operation
    });
 
    return promise; // if you return a promise, the game will wait for the promise to resolve
});

Trigger Action

To trigger an action, you can use the action method.

const service = new MyCustomService();
 
scene.action([
    service.action("myAction", "foo", 123)
]);

or you can wrap the action in a method.

class MyCustomService extends Service<MyCustomActions> {
    myAction(arg0: string, arg1: number) {
        return this.action("myAction", arg0, arg1);
    }
}
 
const service = new MyCustomService();
 
scene.action([
    service.myAction("foo", 123),
 
    service
        .myAction("foo", 123)  // actions are wrapped and chainable
        .myAction("bar", 456), // the game will manage the chain behavior automatically
]);

Accessing Service

After creating the service, you need to register it in the game.

const story = new Story(/* ... */);
story.registerService("gallery", gallery);

And you can access the service using ctx.

// ex. in a component
const {game} = useGame();
const liveGame = game.getLiveGame();
 
const gallery = liveGame.story?.getService<Gallery>("gallery");
 
return (
    {gallery && gallery.unlocked.map((name) => (
        <div key={name}>{name}</div>
    ))}
);

Public Methods

onAction<K extends StringKeyOf<Content>>

Register an action handler.

type MyCustomActions = {
    "myAction": [arg0: string, arg1: number]
};
 
class MyCustomService extends Service<MyCustomActions> {
    constructor() {
        super();
 
        this.onAction<"myAction">("myAction", (ctx: ServiceHandlerCtx, arg0: string, arg1: number) => {
            // custom logic
        });
    }
}
  • key: K - The action key
  • handler: ServiceHandler<Content[K]> - For ServiceHandler, see ServiceHandlerCtx
  • Returns this

Chainable Methods

action<K extends StringKeyOf<Content>>

Trigger an action.

const service = new MyCustomService();
 
scene.action([
    service.action<"myAction">("myAction", "foo", 123)
]);
  • key: K - The action key
  • ...args: Content[K] - The action arguments