@dylibso/xtp
v0.0.0-rc15
Published
XTP for JavaScript
Readme
@dylibso/xtp
The JS SDK for XTP.
QuickStart
The first step to integrating with XTP is to create a Client:
import createClient from '@dylibso/xtp'
const client = await createClient({
appId: process.env.APP_ID, // looks like: 'app_xxxxxxxxxxxxxxxxxxxxxxxx',
token: process.env.XTP_TOKEN, // looks like: 'xtp0_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
logger: console,
useWasi: true
})You'll need your appId and a token.
Note: XTP_TOKEN is a secret and is sensitive. Don't store the TOKEN in source code.
The client instance holds the connection to the API and any plugins you may want to run.
For this reason you probably want to share the instance across your whole application.
If you need to share this across multiple modules, you should use a singleton constructor:
// xtpClient.js
let xtpClient = null;
export async function getXtpClient() {
if (!xtpClient) {
xtpClient = await createClient({
// your client options here
})
}
return xtpClient
}
// mymodule.js
import { getXtpClient } from './xtpClient'
const xtpClient = await getXtpClient()If your plug-in developers may be installing multiple plug-ins on an extension-point,
you'll need a binding name. If not you can skip this step. You can ask which plugins are
bound to an extension-point with the listAvailablePlugins
const GUEST_KEY = 'acme-corp'
const EXT_NAME = 'MyExtensionPoint'
const pluginNames = await xtpClient.listAvailablePlugins(EXT_NAME, GUEST_KEY)
// ['plugin1', 'plugin2']Now you can actually execute the code. This next step will do a few things for you:
- Fetch the latest plug-in from XTP
- Cache it on disk
- Compile and load it
- Cache it in memory
- Execute the function
// assuming your extension point has an export `myExport`
const result = await client.extensionPoints.MyExtensionPoint.myExport(
GUEST_KEY,
'input data', // depends on what plug-in expects
{
bindingName: 'plugin1', // optional item from the list of names we got in the following step
default: 'default value' // can be an object, Uint8Array, or string
}
)
console.log(result)Development Mode (Offline)
For local plugin development, the SDK supports filesystem mode which enables hot reload capabilities without requiring an internet connection or XTP service. The filesystem mode uses lazy loading - extension points and functions are discovered automatically when accessed.
import createClient from '@dylibso/xtp'
const client = await createClient({
filesystem: {
pluginDir: './plugins', // Directory containing your .wasm files
watch: true, // Enable hot reload (default: false)
watchDebounceMs: 100 // Debounce time for file changes
},
logger: console,
useWasi: true
})
// Use exactly like regular XTP client - functions are discovered automatically
await client.extensionPoints.myExtension.process('guestKey', inputData)Directory Structure
Organize plugins by extension point name. Each subdirectory becomes an extension point:
plugins/
├── my-extension/
│ ├── guest1.wasm # Plugin for specific guest "guest1"
│ ├── guest2.wasm # Plugin for specific guest "guest2"
│ └── default.wasm # Fallback for any guest key
└── another-extension/
└── default.wasm # Fallback for any guest keyThe SDK automatically:
- Discovers extension points from directory names
- Maps guest keys to
{guestKey}.wasmfiles or falls back todefault.wasm - Discovers function exports at runtime when called
- Handles missing functions with natural Extism errors
Guest Key Notes:
- Guest keys typically align with external user IDs (e.g., "user123", "acme-corp")
- For testing, you can hard-code any guest key (e.g., "test", "dev", "local")
- Files are resolved as:
{guestKey}.wasm→default.wasm→ not found
Hot Reload Workflow
- Build your plugin:
cargo build --target wasm32-wasi --release - Copy to plugins directory:
cp target/wasm32-wasi/release/my_plugin.wasm ./plugins/my-extension/default.wasm - SDK automatically detects changes and reloads the plugin
- No restart or metadata updates required - next function call uses the updated plugin
Functions are discovered automatically from the WASM exports - no configuration needed.
Production Migration
Remove the filesystem option and add your XTP credentials:
const client = await createClient({
// Remove for production
// filesystem: { ... },
// Add production config
baseUrl: 'https://xtp.dylibso.com',
token: 'your-token',
appId: 'your-app-id',
logger: console,
useWasi: true
})API
fn createClient(opts: XTPClientOptions): Promise<Client>
Create a client with the provided options. In API mode, the Client
fetches the available list of extension points from the XTP API. In filesystem mode,
extension points are discovered lazily from the directory structure. If API calls
fail the promise returned by createClient will reject.
interface XTPClientOptions
baseUrl: defaults tohttps://xtp.dylibso.com.token: An XTP API Token.functions: A{[string]: {[string]: CallableFunction}}map to be exposed to modules.logger: A pino-compatible logger.keepResidentMs: The number of milliseconds to keep plugins "live" after a call. This means the plugin is in-memory and subsequent calls to the same extension point and guest tag will re-use the plugin instance. Resets after each call. Defaults to environment variableXTP_KEEP_RESIDENT_MS, or5000ms.refetchAfterMs: The number of milliseconds to treat local cached plugin data as "fresh"; after which the client will "re-fetch" the installed plugin for the given extension point and tag to revalidate the cache.useWasi: boolean, defaults to true -- whether or not to enable WASIp1 for guests.storage: AnyExtensionStorageinterface. UseslocalStorageon Deno,cacacheon Node.fetch: Anyfetch-compatible function for making requests.filesystem: For offline development mode. Object with:pluginDir: Directory path containing plugin subdirectorieswatch: Enable hot reload file watching (default: false)watchDebounceMs: Debounce time for file change detection (default: 100ms)
class Client
fn client.clone(opts: Partial<XTPClientOptions>): Client
Clone an existing client, bypassing the "load extension points" call during typical startup. Options for the client can be partially overridden.
fn close(): Promise<void>
Close all "live" plugins. May be called repeatedly.
fn inviteGuest(opts: RegisterGuest): Promise<Json>
Invite a guest to develop and install plugins on your XTP application.
interface RegisterGuest
email: string: The guest's email address.name: string: The human-readable name of the guest.guestKey: string: A unique string held by your application to identify the guest.
prop extensionPoints
fn extensionPoints[extName: string][exportName: string](guestKey: string, param: T, defaultValue:T): Promise<T>
const result = await client.extensionPoints.foo.bar('my guest key', {hello: 'world'}, { bindingName: 'my-plugin', default: 'default value' })Call a plugin export installed by a guest (identified by guestKey) at an
extension point.
opts:
Use the bindingName to idenfity the named plugin to return. Defaults to "default".
Use optional default to return a default value
interface ExtensionStorage
Implement your own extension storage for fun and profit! Reach for this interface if the default storage options aren't working for you -- if you'd like to store plugins in a database, for example.
fn getByExtIdGuestKey(extId: string, guestKey: string): Promise<StoredPlugin | null>
Fetch plugin content based on an extId/guestKey pair. Must refer to the
same StoredPlugin content as returned by getByETag.
Return null if not present.
fn getByETag(etag: string): Promise<StoredPlugin | null>
Fetch plugin content based on the ETag value returned by fetching
installations.
Must refer to the same StoredPlugin content as returned by getByExtIdGuestKey.
Return null if not present.
fn store(extId: string, guestKey: string, etag: string, meta: Record<string, string>, content: Uint8Array): Promise<void>
Store plugin content, indexed by both (extId, guestKey) and etag.
interface StoredPlugin
metadata: Record<string, string>: Metadata about the record. Should includeetag,content-type, andlast(a number representing the milliseconds since unix epoch at which the plugin content was last stored.)data: Uint8Array: The plugin content, suitable for passing toWebAssembly.compile.size: number: The size of the stored data.
