Skip to content

Event API ​

js
import { eventWatch, eventIntercept, eventOff } from 'yeow-api';

Subscribe: eventWatch(eventType, handler) (the default) ​

Read-only observation: it does not modify the event, and the runtime does not wait for it. Use it for almost everything (logging, metrics, forwarding, syncing state).

ParameterTypeDescription
eventTypestringEvent type name
handler(e: Readonly<EventType>) => voidHandler (the event object is read-only)

Returns a subscription handle: call it to unsubscribe; await sub.ready waits until the runtime has applied it.

js
const sub = eventWatch('playerJoin', async (e) => {
  log.info(`${await e.player.getName()} joined`);
});
sub();            // unsubscribe
await sub.ready;  // when "subscription is live" matters (triggering the event right after)

Why this is the default: observers never make the event bridge spin and never push an event into its timeout. The event object is typed Readonly, so e.cancelled = true is a type error (use intercept).

Intercept: eventIntercept(eventType, handler) (opt-in) ​

Modify/cancel the event: the runtime waits for the handler to release it (event.complete); fields written are applied when the event completes.

ParameterTypeDescription
eventTypestringEvent type name
handler(e: EventType) => voidWritable handler
options.manualReleasebooleanRelease manually (async decisions)
js
eventIntercept('blockBreak', (e) => {
  if (e.block === 'minecraft:bedrock') e.cancelled = true;
});

// async decision: with manualRelease you receive complete()
eventIntercept('blockBreak', { manualRelease: true }, (e, complete) => {
  fetchPermission(result => {
    e.cancelled = !result.allowed;
    complete({ cancelled: e.cancelled });
  });
});

Use sparingly: the handler runs inside the event dispatch window on the game thread and its writes are applied when the event completes. Returning a Promise means "no modification, release immediately" (use manualRelease for async decisions). Multiple interceptors run serially in registration order.

The two modes ​

Subscribe eventWatchIntercept eventIntercept
Can modify the eventNo (read-only type)Yes
Does the runtime waitNo — delivery continuesYes — waits for event.complete
Can spin the game threadNoYes (5s timeout as backstop)
Use forlogging / metrics / forwarding / state synccancel, change messages, MOTD, permission checks

eventOff(eventType, handler) ​

Unsubscribe/intercept removal (works for both modes). Pass the same handler reference.

js
const handler = (e) => { ... };
eventWatch('playerJoin', handler);
eventOff('playerJoin', handler);

Event Parameters ​

In TS mode there are complete type definitions:

ts
import type { PlayerJoinEvent, BlockBreakEvent } from 'yeow-api';

eventWatch('playerJoin', (e: PlayerJoinEvent) => {
    console.log(e.player.name);   // Player object
    console.log(e.joinMessage);
});

In JS mode event object field names match type table, player field automatically converts to Player object.

Event List ​

Event field value domains (interaction/click action action, damage/teleport cause cause, container type inventoryType etc.) see Value Domain Appendix (action/container types see "Directly Maintained Enumeration List", damage/health regen causes see "Reference Implementation").

Player Events ​

TypeFieldsCancellable
playerJoinplayer, joinMessage
playerQuitplayer, quitMessage
playerChatplayer, message, format✔
playerMoveplayer, from, to✔
playerInteractplayer, action, material, block✔
playerCommandplayer, message✔
playerDeathplayer, deathMessage(Message), deathType✔
playerRespawnplayer, respawnLocation
playerTeleportplayer, from, to, cause✔
playerItemConsumeplayer, item(ItemStack)
playerDropItemplayer, item(ItemStack)✔
playerPickupItemplayer, item(ItemStack)✔
playerBucketFillplayer, bucket✔
playerBucketEmptyplayer, bucket✔
playerExpChangeplayer, amount
playerLevelChangeplayer, oldLevel, newLevel
playerGameModeChangeplayer, newGameMode✔
playerAdvancementDoneplayer, advancement, title(Message)?, description(Message)?
playerToggleSneakplayer, sneaking
playerToggleFlightplayer, flying
foodLevelChangeplayer, oldFoodLevel, newFoodLevel✔

Entity Events ​

TypeFieldsCancellable
entityDamageentity, damage, cause, entityType✔
entityDeathentity, entityType, entityName
entitySpawnentity, entityType, location(Location)✔
entityExplodeentity, entityType, location(Location), blockCount✔
entityRegainHealthentity, amount, reason
entityTargetentity, target✔
projectileLaunchentity, projectileType, shooter?✔
projectileHitentity, projectileType, hitEntity?, hitBlock?

World Events ​

TypeFieldsCancellable
blockBreakplayer, block, location(Location)✔
blockPlaceplayer, block, blockAgainst, location(Location)✔
blockFadeblock, location(Location)
blockGrowblock, location(Location)
blockSpreadblock, location(Location)
blockExplodeblock, location(Location)✔

Inventory Events ​

TypeFieldsCancellable
inventoryOpenplayer, inventoryType, title
inventoryCloseplayer, inventoryType, inventoryId?
inventoryClickplayer, slot, hotbarKey, action, inventoryType, isLeftClick, isRightClick, isShiftClick, clickedItem, cursorItem, inventoryId?✔

inventoryId: When event occurs on Yeow custom Inventory (Inventory.create created), carries that Inventory's handle id (inventory.toString()) — in multi custom Inventory scenarios use e.inventoryId === inventory.toString() to identify click/close attribution. When not custom Inventory (inventory, chest etc.) it's absent.

Server Events ​

TypeFieldsCancellable
serverPingaddress, numPlayers, maxPlayers, motd✔
serverCommandcommand, sender✔
playerResourcePackStatusplayer, status, hash

Permission checks are not events: permissionCheck was extracted from the event system into a dedicated request/response API, onPermissionCheck (no event object, nothing to cancel, and no observer mode).

Event Writeback ​

Write-back only applies to intercept (eventIntercept). Subscribe (eventWatch) is read-only observation: field writes and return values are ignored.

Event results write back through three methods. Writeback only effective in handler's synchronous segment — returning Promise immediately releases event, modifications after await are all invalid (see constraints of each method below).

Method 1: Return Value (mods) — Auto Mode ​

Handler's return value merges into event writeback (mods):

js
eventIntercept('serverPing', (e) => {
    return { motd: 'Hello!' };   // Merge writeback, modify server list MOTD
});

Returning Promise treated as no modification, immediately released, doesn't wait for completion (async handler's async results won't writeback):

js
eventIntercept('blockBreak', async (e) => {
    e.cancelled = true;           // ✅ Synchronous segment effective
    await fetchData();            // Event already released
    return { cancelled: false };  // ❌ Invalid
});

Method 2: Modify Event Parameters (event) — Auto Mode ​

In auto mode directly assigning event fields also writes back (merges with return value, direct assignment takes priority over return value; all fields except cancelled collected):

js
eventIntercept('playerDeath', (e) => {
    e.deathMessage = { text: '§cA hero has fallen.' };  // Writeback death message (Message object or string)
});

eventIntercept('serverPing', (e) => {
    e.motd = 'Hello!';            // Equivalent to return { motd }
});

eventIntercept('blockBreak', (e) => {
    e.cancelled = true;           // Cancel (cancellable event)
});

TIP

The motd write-back in serverPing is not persisted: it only affects that ping response. Meanwhile setMotd writes to server.properties (survives restarts) — when you need a dynamic MOTD without overwriting the owner's configuration, use the serverPing write-back instead of setMotd.

Fields supporting writeback (runtime actually applies; other field assignments sent but runtime ignores — read-only fields):

EventWriteable FieldsDescription
playerJoinjoinMessageJoin message
playerQuitquitMessageQuit message
playerChatmessage / formatChat content / format
playerCommandmessageCommand string (including /)
playerMove / playerTeleporttoTarget position: {x, y, z, yaw?, pitch?, world?} (world defaults to current world; from / cause read-only)
playerRespawnrespawnLocationRespawn position (same shape as above)
playerDeathdeathMessageMessage object {key, args} / {text} or string
foodLevelChangenewFoodLevelNew food level
entityDamagedamageDamage amount
entityRegainHealthamountRecovery amount
entityTargettargetTarget entity UUID or null (clear target)
inventoryClickclickedItem / cursorItemClicked item / cursor item: {type, amount?} (cursorItem's amount: 0 means clear cursor)
serverPingmotd / maxPlayers / numPlayers / iconSee event table (numPlayers requires the dispatched instance to be Paper's PaperServerListPingEvent — true on both platforms in practice, since Folia derives from Paper; only a base-class ServerListPingEvent makes it read-only)
All cancellable eventscancelledCancel

Method 3: Manual Mode complete(mods) ​

After setting { manualRelease: true }, handler receives (event, complete), controls when to end event by calling complete(result). Suitable for scenarios needing to asynchronously obtain data before deciding result:

js
eventIntercept('blockBreak', { manualRelease: true }, (e, complete) => {
    doAsyncCheck((result) => {
        e.cancelled = result;
        complete({ cancelled: result });
    });
});

complete only takes effect once, subsequent calls ignored.

Selection Guide ​

ScenarioRecommendedDescription
Only trigger logic (send message, log, API call)Auto + asyncDoesn't block main thread, freely use async API
Need to synchronously decide result (cancel, change death message, MOTD etc.)Auto + sync handlerDirectly assign event fields (or return mods)
Need to asynchronously obtain data before deciding resultManual mode + complete()User actively controls end timing