Get started
Reference

Studio handlers

Studio handlers

@mountaya/studio declares 253 verbs, each with a name, a Zod input and a capability. @mountaya/studio/handlers implements them, on an opt-in subpath so a read-only surface never pulls a write path in.

Opt-in subpath

Register every handler

createStudioToolBus() builds the studio's bus in one call, with every default handler registered. Every dependency a handler needs arrives on the tool context at dispatch time.

import { createStudioToolBus } from "@mountaya/studio/handlers";

const bus = createStudioToolBus();

That bus answers every published verb. A dispatch validates its input, checks the capability, raises any confirmation the verb declares, and returns a structured result. A refusal is a result, not a throw.

It is typed by StudioToolInputs, what each verb takes, and StudioToolOutputs, what each answers, so a dispatch by name returns that verb's own data: read a member of it after ok, with no cast. The studio and mountaya namespaces are reserved on it, so unregister never takes a studio verb off.

Find the handler for a job

A tool name opens with the noun it acts on.

To do thisDispatch
Move the camera, fit bounds, locate a viewerstudio.map.*, studio.geolocate.*
Switch terrain, toggle overlaysstudio.terrain.*, studio.overlay.*
Route, edit, publish and export an itinerarystudio.itinerary.*, studio.segment.*
Draw and publish a zone, attach it to a routestudio.zone.*
Group itineraries and zonesstudio.collection.*
Share a link, build an embedstudio.shareLink.*, studio.embed.*
Search places, POIs, street viewstudio.search.*, studio.streetview.*
Drive panels, tabs, alertsstudio.panel.*, studio.tabs.*, studio.alert.*
Apply a peer's change, hold a lockstudio.collab.*
Direct a camera run for a videostudio.video.*

Publish and withdraw a zone

studio.zone.publish freezes a zone's latest saved version into the copy that collection embeds and collection share links serve, and studio.zone.unpublish withdraws that copy. Both take { zoneId } and are the zone's twins of studio.itinerary.publish and studio.itinerary.unpublish. Both are hidden from the co-pilot, since what the public sees is the reader's decision.

Publishing refuses before anything is sent: a zone the session knows is personal with forbidden, a viewer not granted publish with upgrade-required, a zone a collaborator holds with collab-locked, and an edit of anything else, or a reshape of the zone changed and not saved, with edit-in-progress. The reader's own reshape, armed and saved, is no refusal. A first publish past the agreement's published-zones limit is the API's to refuse, and arrives as limit-reached.

Withdrawing checks no lock, no open edit and no entitlement: it freezes nothing, and an organization takes down what it published whatever its agreement holds now. It asks first, behind an acknowledgement, because every collection embed and share link showing the zone goes blank at once.

A zone is public or private apart from its publication. studio.zone.create and the create branch of studio.zone.editCommit take isPrivate, true when left out, so a create lands private. studio.zone.update takes it to change a saved zone's visibility, which takes effect at once, published or not.

Replace or wrap one handler

The handlers are a plain object keyed by tool name, so replacing one is a spread. The other 252 stand. Hand the map to createStudioToolBus as handlers:

import { createStudioHandlers, createStudioToolBus } from "@mountaya/studio/handlers";

const bus = createStudioToolBus({
  handlers: {
    ...createStudioHandlers(),
    "studio.itinerary.export": exportThroughOurOwnPipeline,
  },
});

A bus you build yourself with new ToolBus takes the same map through registerStudioTools:

import { registerStudioTools } from "@mountaya/studio";
import { createStudioHandlers } from "@mountaya/studio/handlers";

registerStudioTools(registrableBus, {
  ...createStudioHandlers(),
  "studio.itinerary.export": exportThroughOurOwnPipeline,
});

Wrapping keeps the default: log the dispatch, refuse it on a condition of your own, then delegate. Use overrideStudioHandler. A default is a bare function or a definition with policy beside one. A wrapper written against the first shape drops the second's requiresConfirmation.

import { createStudioHandlers, overrideStudioHandler } from "@mountaya/studio/handlers";

const handlers = overrideStudioHandler(
  createStudioHandlers(),
  "studio.itinerary.delete",
  (current) => {
    const wrapped = typeof current === "function" ? current : current?.handler;
    if (wrapped === undefined) {
      throw new Error("no default to wrap");
    }

    return {
      handler: (ctx, input) => {
        recordIntent(input);

        return wrapped(ctx, input);
      },
      requiresConfirmation: true,
    };
  },
);

Classify the errors they raise

A handler that cannot recognise a failure re-throws it, and the bus classifies it with its recognizers. createStudioToolBus holds STUDIO_HANDLER_RECOGNIZERS, the subset a handler applies on its own, and asks yours after them, so yours never shadow one of the studio's.

import { createStudioToolBus } from "@mountaya/studio/handlers";

const strictBus = createStudioToolBus({ recognizers: ourOwnRecognizers });

That set omits one arm: telling a 402 that needs a card from one the agreement does not cover. Add your own where those route differently.

What else the subpath publishes

  • acquireCollabLock and releaseCollabEntity: a collaborative edit's soft lock.
  • createVideoDirectorBus: a camera-scoped bus advertising only studio.video.*.

The handlers write through a client you supply and a cache you bind, and choose neither. Every dependency has a defined absence. A studio mounted without one still runs, and refuses the verbs that needed it.

Next steps