Skip to content

Adapters guidelines and rules

Alexander Cerutti edited this page May 1, 2023 · 4 revisions

As per the current web standards, the current situation in web media handling could still be described as a whole mess and a far west (in the author's opinion). So, to avoid further contributing to the entropy of the web itself, adapters should follow some well-precise rules, built to make them safer.

Each adapter should be a class

Each adapter must be a class, which will get instanced by @sub37/server when used. Each session creates a new adapter instance. By doing this way, different adapters could hold session information without polluting the module scope. Yet, this is still possible, but it will be your choice to decide whether risk suffering by doing this.

Each adapter should own a meaningful name

Adapters should own a meaningful class name, that will describe exactly which format the adapter will support. The adapter also shouldn't be an anonymous class**.

This is not strict and necessary but very suggested. Starting from v1.1, BaseAdapter overrides the toString() method to print the class name when invoked. This happens when emitting errors or printing logs. Therefore, having a meaningful name will allow you to have better logs.

Each adapter should extend a base adapter

As per the title, @sub37/server exposes a class, BaseAdapter which must be used by every adapter. This base class will create the required methods so that, if they do not get overridden, they will end up throwing a meaningful error for adapters developers and users.

Each adapter must override some base adapter methods and properties.

There are some methods and properties that the server requires to override.

  1. supportedType static property. This property will return the mime-type of the supported format. That will be used to select the adapter when a track is passed. An adapter will be selected if the track's mimeType and the adapter supportedType will match.

The mimeType will never get changed by @sub37/server, but if you are concerned by this possibility, you can use a getter as well.

import { BaseAdapter } from "@sub37/server";

export class MyAdapter extends BaseAdapter {
	public static get supportedType(): string {
		return "...";
	}

	...
}
  1. toString method. This override will let some errors have a meaningful error message, containing your adapter's name. An adapter will be completely rejected if toString is not overridden.

Warning Starting from v1.1, this won't be needed anymore. ⚠

Both static and instance toString methods are required to get overridden.

import { BaseAdapter } from "@sub37/server";

export class MyAdapter extends BaseAdapter {
	...

	public static toString(): string {
		return "MyAdapter";
	}

	public toString(): string {
		return "MyAdapter";
	}

	...
}
  1. parse instance method. This is the core of your logic. .parse() is the entry point of your adapter, and will be invoked when some content is ready to be parsed. This method will receive the content passed to the tracks.

As the content can be of several formats, for Typescript folks, the parameter is marked to be unknown, so that you can specify the type when overriding the .parse() method.

Each parse() invocation always expects to receive a ParseResult back.

Not returning a ParseResult will make your parsed result get discarded and an error getting thrown.

A ParseResult is a container that will expose two properties: data, which is an Array<CueNode>, and errors, which is an Array<ParseError>.

ParseError is an object with the following structure:

interface ParseError {
	error: Error;
	isCritical: boolean;
	failedChunk: unknown;
}

errors will therefore contain any error that will happen during the parsing phase, whether it is critical or not. ParseError.prototype.error will contain an Error instance about what happened. A critical error is expected to make the adapter perform an early return and to have the isCritical flag marked as true. You can populate failedChunk to let the error carry more information about that failure.

data will instead contain the CueNode you were able to create through your parsing.

ParseResult (both instance and type) and ParseError (as type) are exposed as a property of BaseAdapter.

import { BaseAdapter } from "@sub37/server";

export class MyAdapter extends BaseAdapter {
	...

	public override parse(content: unknown): BaseAdapter.ParseResult {
		/** Non critical errors **/
		const failures: BaseAdapter.ParseError[] = [];

		let result: CueNode[];

		try {
			result = attemptConvertIntoCueNodeSomehow(content);
		} catch (err: unknown) {
			/** Critical error */
			return BaseAdapter.ParseResult(undefined, [
				{
					error: new CriticalError(),
					failedChunk: "...",
					isCritical: true,
				},
			]);
		}

		return BaseAdapter.ParseResult(result, failures);
        }
    }
}