npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ksaa-nlp/gamehub-sdk

v0.9.0

Published

ESM SDK that lets GameHub-hosted games talk to the platform (auth, sessions, scores, multiplayer). Drops into Node, Angular, browsers, or a `<script>` tag.

Readme

GameHub SDK v0.4.0 — Official Developer Guide

GameHub SDK is an ESM SDK used by GameHub-hosted games to communicate with the platform for authentication, sessions, score submission, league matches, questions, and multiplayer rooms.

It runs in any modern JavaScript environment — browser, Node, Angular Universal / SSR, Vite, webpack, esbuild, Bun. It also ships a self-contained IIFE bundle for plain <script>-tag use.

Install

Package-based games (Angular, React, Vue, Phaser, Vite, webpack, ...)

npm install @ksaa-nlp/gamehub-sdk
import { createSdk, type Sdk } from '@ksaa-nlp/gamehub-sdk';

const sdk: Sdk = createSdk();

await sdk.ready();
await sdk.submitScore(1500);

Importing the package has no side effectscreateSdk() builds an SDK instance you control. Calling any method that needs platform state (e.g. ready(), submitScore()) lazily triggers the bootstrap handshake the first time it's needed.

The package is ESM-only. Modern Node (≥18), Angular CLI, Vite, webpack 5+, esbuild, Bun, and Rollup all consume ESM natively. Tooling that requires CJS require() needs a transpile step or a dynamic import().

Server-side rendering (Angular Universal, Next-style SSR, Node services)

The package is safe to import on the server — no window / document access happens at evaluation time. Methods called server-side return sensible defaults instead of throwing:

const sdk = createSdk();

// Safe on the server — no ReferenceError, no exception.
sdk.isInIframe();                 // false
sdk.getBootstrap().isBootstrapped; // false
sdk.notifyComplete({ score: 0 }); // silently no-ops
await sdk.ready();                // resolves to false

You don't need isPlatformBrowser / typeof window gates around the import or around createSdk().

GameHub ZIP uploads

When you upload a ZIP game, GameHub injects the SDK into the entry HTML automatically:

<script src="/sdk/gamehub-sdk.js" data-game-slug="your-game-slug"></script>

This loads the IIFE bundle, attaches window.GameHubSDK, and runs the auto-init handshake on DOMContentLoaded. Do not add your own wrapper layer around the SDK. Use the public methods directly and wait for ready().

Local standalone testing

If you run the game outside GameHub and are not bundling the npm package, add this script tag only for local testing:

<script src="https://gamesbeta.ksaa.gov.sa/sdk/gamehub-sdk.js"></script>

Remove the tag before uploading if your build already imports the package or if GameHub will inject it.

Required boot pattern

There is no separate init() call. The SDK auto-detects game context from the script tag (or your import), URL, and host postMessage bootstrap payload — await sdk.ready() runs the handshake and resolves when it's done.

import { createSdk } from '@ksaa-nlp/gamehub-sdk';

const sdk = createSdk();
await sdk.ready();

const bootstrap = sdk.getBootstrap();
console.log({
  version: sdk.version,
  gameSlug: bootstrap.gameSlug,
  leagueId: bootstrap.leagueId,
  matchId: bootstrap.matchId,
  peerId: bootstrap.peerId,
  room: bootstrap.room,
});

For <script>-tag games:

await window.GameHubSDK.ready();
const bootstrap = window.GameHubSDK.getBootstrap();

Bootstrap data

sdk.getBootstrap() returns the current runtime state:

{
  gameSlug: string | null;
  authToken: string | null;
  leagueId: string | null;
  gameId: string | null;
  matchId: string | null;
  match: MatchContext | null;
  participant1Id: string | null;
  participant2Id: string | null;
  participant1MemberIds: string[];
  participant2MemberIds: string[];
  playerName: string | null;
  peerId: string | null;
  room: BootstrapRoom | null; // null for non-multiplayer sessions
  isBootstrapped: boolean;
}

For live peer/room state, use sdk.multiplayer.getRoom(), sdk.multiplayer.getPeers(), and the peer_joined / peer_left events — bootstrap intentionally doesn't expose live counts because they'd be stale by the time you read them.

Score submission

Call submitScore when a standalone game round ends:

const result = await sdk.submitScore(score, {
  mode: 'single',
  correctAnswers: 8,
  totalQuestions: 10,
  durationMs: 92000,
  level: 3,
});

if (result.skipped) {
  // In an online room, this peer is not the host. The host will submit.
} else if (!result.success) {
  console.error(result.error);
}

submitScore returns a SubmitScoreResult:

{
  success: boolean;
  accepted?: boolean;
  timeTaken?: number;
  error?: string;
  skipped?: boolean;
}

Inside a multiplayer room, only the host can write scores. Guests and viewers receive { success: true, skipped: true } without a network write.

League match integration

When a game is opened from a league match, the host supplies leagueId, gameId, and matchId. The SDK uses these values to start a scoped match session.

Fetch participant names and team members:

await sdk.ready();

const match = await sdk.getMatchContext();
if (match) {
  renderSideA(match.participant1.name, match.participant1.members);
  renderSideB(match.participant2.name, match.participant2.members);
}

MatchContext:

{
  id: string;
  leagueId: string;
  gameId: string;
  participant1: {
    participantId: string;
    name?: string;
    members?: Array<{ leaguePlayerId: string; fullname?: string; avatar?: string }>;
  };
  participant2: {
    participantId: string;
    name?: string;
    members?: Array<{ leaguePlayerId: string; fullname?: string; avatar?: string }>;
  };
  status: string;
}

Submit the match winner and scores:

await sdk.submitMatchResult({
  winnerId: match.participant1.participantId,
  scores: {
    [match.participant1.participantId]: 12,
    [match.participant2.participantId]: 8,
  },
  reason: 'normal_finish',
  details: {
    durationMs: 94000,
  },
});

For a match-specific score without a full bracket result:

await sdk.submitMatchScore(1200, {
  correctAnswers: 10,
  totalQuestions: 12,
});

In multiplayer league matches, the host should submit the match result. If shared game code calls submitMatchResult from a guest or viewer, the SDK returns skipped.

Questions

Fetch published questions for the active game:

const questions = await sdk.getQuestions({
  difficulty: 'medium',
  category: 'science',
  limit: 20,
});

The SDK uses gameId when provided by the host. Otherwise it falls back to the game slug.

Question shape:

{
  id: string;
  difficulty?: 'easy' | 'medium' | 'hard' | string;
  category?: string;
  prompt: string;
  choices: string[];
  answer: string | number;
  explanation?: string;
  points: number;
}

Invalid options or API failures return an empty array.

Multiplayer model

GameHub multiplayer is host-authoritative:

| Concern | Owner | | -------------------------------- | --------------------------------------------- | | Random grids, seeds, decks | Host | | Turn order and round advancement | Host | | Click/move validation | Host | | Player input | Local player sends intent | | Final score and match result | Host only | | Timer | Host broadcasts or sends an absolute deadline |

Guests send intent with send. The host validates and publishes resolved state with broadcast.

Create and join

const room = await sdk.multiplayer.createRoom({
  maxPlayers: 2,
  passcode: '1234',
});

console.log(room.roomCode, room.shareUrl);
await sdk.multiplayer.joinRoom('AB12CD', {
  passcode: '1234',
  displayName: 'Player 2',
});

await sdk.multiplayer.joinRoomByLink(window.location.href);

// Read-only guest:
await sdk.multiplayer.joinRoom('AB12CD', { asViewer: true });

Auto-join on boot

The SDK auto-joins when it can detect a pending room — you don't need to wire any of this yourself:

| Signal | Where it comes from | | ------------------------------------------------------------ | ---------------------------------------------------------------------- | | ?mp=<shareId> on the page URL | QR scan or shared link — SDK auto-calls joinRoomByLink. | | ?roomCode=<CODE>&playerName=<name> | Host UI forwarded this after join-token — SDK auto-calls joinRoom. | | GAMEHUB_SDK_BOOTSTRAP postMessage (shareId / roomCode) | Host page pushed them into the iframe. |

By the time await sdk.ready() resolves, your getBootstrap() already contains peerId and room, and the socket is connecting. The bootstrap does not contain the peer list or live room state — those would be stale by the time you read them. Use multiplayer.getRoom(), multiplayer.getPeers(), and listen to connected / room_joined / peer_joined / peer_left for live data.

Send and receive game events

Guests send intent with send(). The host validates and publishes resolved state with broadcast() — host-only; returns false for guests and viewers.

const mp = sdk.multiplayer;

function onCellClick(cellId: string) {
  mp.send('cell_click', { cellId });
}

mp.on('cell_click', ({ fromPeerId, payload }) => {
  if (!mp.isHost()) return;
  const result = resolveMove(fromPeerId, payload.cellId);
  mp.broadcast('cell_resolved', result);
});

mp.on('cell_resolved', ({ payload }) => {
  applyResolvedMove(payload);
});

Role helpers

sdk.multiplayer.getRole(); // 'host' | 'player' | 'viewer' | null
sdk.multiplayer.isHost();
sdk.multiplayer.isViewer();
sdk.multiplayer.selfId();
sdk.multiplayer.getRoom();
sdk.multiplayer.getPeers();
sdk.multiplayer.isConnected();
sdk.multiplayer.leaveRoom();

Events

Subscribe with sdk.multiplayer.on(eventName, handler). Use '*' to observe all events.

| Event | Payload | Meaning | | -------------- | ------------------------------------------------- | --------------------------- | | connected | { roomCode, peerId } | WebSocket opened | | disconnected | { roomCode } | WebSocket closed | | reconnecting | { attempts, delayMs } | SDK is reconnecting | | room_joined | { peerId, role, peers } | Local peer joined | | peer_joined | { peerId, role, ... } | Another peer joined | | peer_left | { peerId } | A peer left | | room_closed | { reason } | Host/server closed the room | | room_left | { reason } | Local peer left | | error | { code } | Server rejected a frame | | message | { eventName, payload, fromPeerId, seq, sentAt } | Any custom game event |

Custom game events are also emitted by their own name:

mp.on('round_resolved', ({ payload, fromPeerId, seq }) => {
  applyRound(payload);
});

The SDK drops late or duplicate game-event frames using the server-provided seq per sending peer.

Public API reference

Core:

  • version: string
  • ready(): Promise<boolean>
  • isInitialized(): boolean
  • isAuthenticated(): boolean
  • hasActiveSession(): boolean
  • startSession(): Promise<StartSessionResult>
  • submitScore(score, meta?): Promise<SubmitScoreResult>
  • submitMatchScore(score, meta?): Promise<SubmitScoreResult>
  • submitMatchResult(result?): Promise<SubmitScoreResult>
  • playAgain(): Promise<{ success: boolean; token: string | null }>
  • notifyComplete(data?): void
  • requestClose(): void
  • logEvent(eventName, eventData?): void
  • getGameSlug(): string | null
  • isInIframe(): boolean
  • getBootstrap(): BootstrapData
  • getMatchContext(): Promise<MatchContext | null>
  • getQuestions(options?): Promise<Question[]>

Multiplayer:

  • multiplayer.createRoom(options?)
  • multiplayer.joinRoom(roomCode, options?)
  • multiplayer.joinRoomByLink(link, options?)
  • multiplayer.leaveRoom(reason?)
  • multiplayer.send(eventName, payload?)
  • multiplayer.broadcast(eventName, payload?)
  • multiplayer.on(eventName, handler)
  • multiplayer.getRoom()
  • multiplayer.getPeers()
  • multiplayer.isConnected()
  • multiplayer.getRole()
  • multiplayer.isHost()
  • multiplayer.isViewer()
  • multiplayer.selfId()

Type imports

Every public type is exported as a named ESM type from the package root. Import what you need:

import type {
  Sdk,
  BootstrapData,
  BootstrapRoom,
  MatchContext,
  MatchParticipant,
  MatchResultPayload,
  RoomInfo,
  Peer,
  RoomOptions,
  JoinOptions,
  CreateRoomResult,
  JoinRoomResult,
  ScoreMetadata,
  Question,
  QuestionOptions,
  StartSessionResult,
  SubmitScoreResult,
  MpMessage,
  MpEventHandler,
} from '@ksaa-nlp/gamehub-sdk';

Pause and resume

If the host pauses/resumes the iframe, define these optional handlers:

window.onGameHubPause = function () {
  pauseGame();
};

window.onGameHubResume = function () {
  resumeGame();
};

Common issues

window.GameHubSDK is undefined

You're on the <script>-tag path but the bundle didn't load. Either:

  • The script tag is missing from your HTML — re-add <script src="/sdk/gamehub-sdk.js"> (or the local-testing CDN URL).
  • You're inside a bundled app and never imported the SDK — import @ksaa-nlp/gamehub-sdk and call createSdk() instead. The npm path doesn't attach window.GameHubSDK; that's only for the script-tag flow.

import sdk from '@ksaa-nlp/gamehub-sdk' fails or returns undefined

The default-import singleton was removed in v0.2.0. Use the named factory:

import { createSdk } from '@ksaa-nlp/gamehub-sdk';
const sdk = createSdk();

ReferenceError: window is not defined at import time

You're on a version older than v0.3.0. Upgrade to v0.4.0 or later — importing the package no longer touches window / document. If you still see this on a current version, something is importing the script-tag entry (@ksaa-nlp/gamehub-sdk/browser or dist/gamehub-sdk.js) on the server. Use the default . entry instead.

Cannot use import statement outside a module / CJS require() fails

The package is ESM-only. Set "type": "module" in your package.json, or use a dynamic import() from CJS code.

Score returns skipped

The current peer is not the multiplayer host, or it joined as a viewer. This is expected and prevents duplicate writes.

Match context is null

The game was not launched with leagueId and matchId, or the match endpoint could not resolve the match. Fall back to normal game UI.

Questions return []

No published questions matched the filters, options were invalid, or the API request failed. Treat this as the empty/degraded state.

Guest state differs from host

Move all random generation and final state transitions to the host. Guests should send intent only and wait for broadcasted resolved events.

Versions