Skip to content

Browser Client

Axora ships its browser client as TypeScript source inside the Composer package. It exposes two package entry points:

  • @artisan-toolbox/axora for the framework-independent client;
  • @artisan-toolbox/axora/vue for the optional Vue composable.

Register the Composer-installed directory as a local dependency in the application’s package.json:

package.json
{
"dependencies": {
"@artisan-toolbox/axora": "file:vendor/artisan-toolbox/axora"
}
}

Run Composer before npm so the local package path exists:

Terminal window
composer install
npm install

This is the recommended integration. Vite, TypeScript, and IDEs resolve Axora through the standard node_modules package mechanism. Root imports, the /vue subpath, event-name suggestions, and inferred payload types therefore use the same metadata.

The consuming application owns the lockfile generated by npm. Axora itself does not track or distribute a package-lock.json because it is installed through Composer and is not published to an npm registry.

An application that does not want a local npm dependency may point Vite directly at the Composer package:

vite.config.ts
import { defineConfig } from "vite";
import path from "node:path";
export default defineConfig({
resolve: {
alias: {
"@artisan-toolbox/axora": path.resolve("vendor/artisan-toolbox/axora"),
},
},
});

A Vite alias configures the bundler only. Add matching TypeScript paths so editors can resolve the same imports:

tsconfig.json
{
"compilerOptions": {
"paths": {
"@artisan-toolbox/axora": ["vendor/artisan-toolbox/axora"],
"@artisan-toolbox/axora/*": ["vendor/artisan-toolbox/axora/*"]
}
}
}

The package’s exports, root types, and typesVersions metadata provide type information after the alias target is resolved. A Vite alias alone is not a portable IDE configuration.

Call initializeAxora() once from the application entry point:

resources/js/app.ts
import { initializeAxora } from "@artisan-toolbox/axora";
initializeAxora();

The default public endpoint is /_axora/connect. Override it only when the reverse proxy uses another public path:

initializeAxora({
endpoint: "/events",
});

withCredentials defaults to true, ensuring the browser includes Laravel’s session cookie. Same-origin deployment is strongly recommended:

initializeAxora({
endpoint: "/events",
withCredentials: true,
});

Initialization is idempotent. Calling initializeAxora() or axora.connect() again while connected returns the existing client and does not open another EventSource. Options passed by later calls do not replace an already-active connection; disconnect first when intentionally changing the endpoint.

Import the shared axora instance wherever an event is consumed:

resources/js/toasts.ts
import { axora } from "@artisan-toolbox/axora";
const stop = axora.on("toast", (payload, event) => {
showToast(payload.type, payload.message);
console.log(event.lastEventId);
});

The returned function removes that listener:

stop();

Listeners can be registered before or after initialization. This makes module loading order unimportant:

resources/js/app.ts
import "./toasts";
import { initializeAxora } from "@artisan-toolbox/axora";
initializeAxora();

The client maintains one listener registry independently from the active stream:

  • the same callback registered twice for the same event is attached once;
  • the same callback may listen to different event names independently;
  • disconnect() closes the stream but preserves registered listeners;
  • reconnecting attaches each stored listener exactly once;
  • removing a listener while disconnected prevents it from returning on reconnect.
  • each listener ignores the 256 most recent repeated, non-empty SSE event identifiers.

Call disconnect() only when the application should stop listening entirely:

axora.disconnect();

Native EventSource handles ordinary network and server-initiated reconnects automatically. Do not implement a second reconnect timer around Axora.

Recent identifier suppression prevents one client from handling the same publication twice when it reconnects between daemons that each held a process-local pending copy. The history is bounded per listener and exists only for the lifetime of that JavaScript listener; Axora remains a transient transport rather than a durable exactly-once system.

Declare the application’s event map once to get event-name autocomplete and inferred payloads everywhere:

resources/js/axora.d.ts
import "@artisan-toolbox/axora";
declare module "@artisan-toolbox/axora" {
interface AxoraEvents {
"document.loaded": {
document: number;
};
toast: {
type: "success" | "error";
message: string;
};
}
}

Keep this declaration in a directory included by the application’s tsconfig.json. The shared client and Vue composable both use the augmented interface:

axora.on("document.loaded", ({ document }) => {
refreshDocument(document);
});

Events do not have to be added to the global catalog. Supply a payload type explicitly for a local or one-off event:

axora.on<{ percentage: number }>("export.progress", ({ percentage }) => {
updateProgress(percentage);
});

The event map is a compile-time contract, not network validation. Validate payloads at runtime when an endpoint or publication source is not fully trusted.

Most applications should use the shared client. Create an isolated connection only when it intentionally uses a different endpoint or lifecycle:

import { createAxora } from "@artisan-toolbox/axora";
type AdminEvents = {
"audit.created": {
audit: string;
};
};
const adminAxora = createAxora<AdminEvents>({
endpoint: "/admin/events",
});
adminAxora.on("audit.created", ({ audit }) => {
console.log(audit);
});
adminAxora.connect();

Each isolated client owns a separate EventSource. Creating one accidentally defeats Axora’s single-connection design, so keep the shared axora instance as the default.

Vue components may subscribe directly:

<script setup lang="ts">
import { useAxora } from "@artisan-toolbox/axora/vue";
useAxora("toast", ({ type, message }) => {
showToast(type, message);
});
</script>

Use the grouped form when one component listens for several names:

<script setup lang="ts">
import { useAxora } from "@artisan-toolbox/axora/vue";
const { on } = useAxora();
on("toast", ({ type, message }) => {
showToast(type, message);
});
on("document.loaded", ({ document }) => {
refreshDocument(document);
});
</script>

Both forms subscribe to the shared client initialized by initializeAxora(). The composable removes every listener it registered through Vue’s onUnmounted lifecycle hook. Its returned cleanup function remains available when a component needs to stop listening earlier.