SlayQ Client

Creating the Client

In order to dispatch or receive events in whatever stack you are working with, you'll need to create a client. Here's an example:

import { SlayQClient } from "@slay-pics/slay-q";
import { SlayQSupabaseDriver } from "@slay-pics/slay-q-supabase";

import { sleepTestEvent, type SleepTestEvent } from "~/server/slay-q/testing/sleep-test";
import { cronTestEvent, type CronTestEvent } from "~/server/slay-q/testing/cron-test";
import { cancelOnTest, type CancelOnTestEvent } from "~/server/slay-q/testing/cancel-on-test";

export type TestingEvents = {
  "testing/sleep-test": z.infer<typeof SleepTestEvent>;
  "testing/cron-test": z.infer<typeof CronTestEvent>;
  "testing/cancel-on-test": z.infer<typeof CancelOnTestEvent>;
};

export const TestingFunctions = [
  sleepTestEvent,
  cronTestEvent,
  cancelOnTest,
];

export const defaultSlayQClient = new SlayQClient<TestingEvents>({
  driver: new SlayQSupabaseDriver(),
  endpoint: "http://localhost/api/slay-q",
  functions: [
    ...TestingFunctions,
  ],
});

There's a bit to unpack here.

First we are creating a TestingEvents type that maps an event to the event's data type (inferred from the event's data's schema). This type is then passed to the SlayQClient so that any calls to sendEvent are typed.

Second thing we are doing is creating a const called Testing Functions that is just an array of all of our testing functions. It seems silly to do it for just 3 functions, but when this gets larger and you're pulling in functions for different parts of your app, you'll want to keep this as organized and neat as possible. We have 180+ functions in use on Slay.

The endpoint is where slay-q-server can callback to when it has an event for your app to process. Obviously this should be specified with configuration or environment variables.

The driver is the interface with the database that the client will use. Slay Q provides three base packages with different drivers:

Dispatch Events

Once you've created the client, dispatching events is easy:

await defaultSlayQClient.sendEvent("products/video-product/unlocked", {
  videoProductId: videoProduct.id,
  profileId: profile.id,
  userId: profile.user_id,
});

In this example, we're dispatching an unlocked event for a video product with the associated data needed for the event.

Receiving Events

This is where it will get trickier because this part is dependent entirely on whatever stack or backend you are using. For us, that is Nuxt.

We create a nitrojs handler at /api/slay-q/index.post.ts that looks like:

import { useValidatedBody } from "h3-zod";
import { createError, H3Event, sendError } from "h3";
import { SlayQReceiveEventPayloadSchema } from "@slay-pics/slay-q";
import { defaultSlayQClient } from "~/lib/slay-q/default-slay-q-client";

export default defineEventHandler(async event => {
  const sig = getHeader(event, "X-SlayQ-Signature");
  if (!sig) {
    sendError(event, createError({ statusCode: 400, statusMessage: 'Invalid signature' }));
    return;
  }

  const body = await useValidatedBody(event, SlayQReceiveEventPayloadSchema);

  await defaultSlayQClient.receiveEvent(sig, body);

  return {
    status: "ok",
  };
});

First we get the X-SlayQ-Signature header which contains the signature of the request. Note that you will need to have the environment variable SLAY_Q_WORKER_SECRET defined in your .env or wherever you are specifying environment variables.

We then fetch the body of the request (as a parsed JSON object) with useValidatedBody. We've passed in the SlayQReceiveEventPayloadSchema schema to validate the payload against.

We then call the receiveEvent method on the SlayQClient instance which handles the rest.

This should be pretty easy to adapt other frameworks.