Architecture

How the SDK, the bridge and the GAMEE platform actually talk.

This page explains what flows on the wire and where each piece lives. Read it once and the rest of the SDK should make sense.

System components#

  • HTML5 game — the code you wrote. Calls gamee.init(), updateScore(), subscribes to gamee.on('pause', …).
  • GAMEE SDK — the typed public surface plus a platform-specific bridge picked at boot.
  • Platform — gamee.com on web, the iOS app, or the Android app. Embeds the game inside an iframe, WKWebView, or WebView.

The SDK abstracts the transport so you write the same code for all three platforms.

End-to-end session trace#

A real game’s lifetime on the wire, traced top-to-bottom.

Request
The game asks the platform to do something. Travels game → platform.
Response
The platform's reply to a specific request. Travels platform → game.
Event
The platform pushes a notification to the game. Travels platform → game, no reply expected.

The first three steps are the handshake — every game does this. Everything after is your game’s choice. start / pause / resume are the only events the platform reliably fires; the rest are opt-in features wired through capabilities.

Platform detection#

createBridge() decides which transport to use the moment the SDK is constructed. Rules from bridge/detect.ts, evaluated in order:

User agent containsAt the top-level?Detected platform
iPhone / iPad / iPodyes (no iframe)ios
gamee/X.Y.Zandroid
Mobi/Android/iPhone…mobile_web
anything elseweb

Note: native webviews (iOS WKWebView, Android WebView) load the game as the top-level document — they don’t wrap it in an iframe. So ios is selected when the UA is iPhone-like and there is no parent frame; an iOS device viewing the game inside an iframe (e.g. play.gamee.com on mobile Safari) gets mobile_web instead.

mobile_web and web both use the postMessage bridge; ios and android each use a native bridge.

You can override detection by injecting a bridge yourself (createSdk({ bridge: myBridge })) — that’s how the emulator and MockBridge work.

Is there a host at all?#

detectPlatform answers which wire format to use — but a top-level browser opened standalone still resolves to web or mobile_web even when no GAMEE host is on the other end. To answer can I actually talk to anyone?, call the exported helper:

import { hasPlatformContext } from '@gamee/sdk';

if (!hasPlatformContext()) {
  // Page was opened outside the GAMEE platform — render a fallback.
  return;
}

Synchronous, side-effect-free. Returns true when the document is inside an iframe, or when the iOS / Android native handlers are present on window. Always call this before createSdk() / init(); otherwise a standalone load will hang for 30 s and reject with BRIDGE_TIMEOUT. See hasPlatformContext for the exact rules.

The wire format#

There is one envelope that flows in both directions:

{ "request":  { "method": "init", "messageId": 0, "data": { ... } } }
{ "response": { "messageId": 0, "data": { ... } } }
{ "response": { "messageId": 0, "error": { "code": "...", "message": "..." } } }

messageId is a non-negative integer that increments by 1 per outbound message within a single SDK instance (starts at 0). Wire-compatible with legacy gamee-js clients.

request is what an RPC looks like. response is the matching ack. The messageId ties them together — the SDK uses it to resolve the right Promise on your await.

The same envelope shape carries platform → game events. The platform sends a request, the SDK auto-acks it, and then dispatches the matching gamee.on('pause', …) listener. The ack just tells the platform “got it, my game is alive.”

Four traffic patterns#

a) Init handshake#

Game → SDK      : await gamee.init({ capabilities: ['saveState'] })
SDK → Platform  : { request: { method: 'init', messageId: 0,
                               data: { version, capabilities: { saveState: true } } } }
Platform → SDK  : { response: { messageId: 0,
                                data: { platform, locale, country, sound, ... } } }
SDK → Game      : Promise resolves with the GameInitResponse

init() is one-shot per SDK instance — see Methods → init is one-shot.

b) Fire-and-forget signal (updateScore, gameReady, gameOver, …)#

Game → SDK      : gamee.updateScore({ score: 123, playTime: 4.5, checksum: 'sum' })
SDK → Platform  : { request: { method: 'updateScore', messageId: 1, data: {…} } }
                     ↑ no Promise tracked, no timeout, platform ack is ignored

Signal calls do not occupy a slot in the pending registry, do not arm a timeout, and the returned Promise resolves immediately. Safe to call every frame.

c) Data-returning request (showRewardedVideo, purchaseItemWithGems, …)#

Game → SDK      : const r = await gamee.showRewardedVideo()
SDK → Platform  : { request: { method: 'showRewardedVideo', messageId: 2, data: null } }

(...time passes; user watches ad...)

Platform → SDK  : { response: { messageId: 2, data: { videoPlayed: true } } }
SDK → Game      : Promise resolves with { videoPlayed: true }

If the platform never responds within requestTimeoutMs (default 30 s), the Promise rejects with BRIDGE_TIMEOUT.

d) Platform → game event (pause, resume, start, …)#

Platform → SDK  : { request: { method: 'pause', messageId: 7, data: null } }
SDK → Platform  : { response: { messageId: 7 } }     ← auto-ack
SDK → Game      : every gamee.on('pause', …) handler fires

Listeners can throw without breaking dispatch — errors are caught and logged to console.error.

The PendingRegistry#

Every data-returning request is parked in an internal PendingRegistry keyed by messageId until the platform responds. Two safety nets:

  • TimeoutrequestTimeoutMs (default 30 000 ms) rejects forgotten requests with BRIDGE_TIMEOUT.
  • Overflow capmaxPendingRequests (default 256) rejects the oldest pending request with BRIDGE_OVERFLOW to make room. Only data-returning methods count; signals are not tracked.

Both are tunable via SdkOptions.

Why this matters in practice#

  1. The platform is authoritative. init() resolves with the platform’s view of the world: locale, country, save state, mission. Don’t hard-code these — read them from the resolved value every session.
  2. Capabilities gate at the SDK boundary. Calling a gated method without the matching capability throws CAPABILITY_MISSING synchronously, before anything hits the wire.
  3. Errors are typed. Every rejection is a GameeError. Branch on err.code. See error handling.
  4. Events fire after init. The platform won’t dispatch start, pause, etc. until the handshake succeeds — and you must subscribe before gameReady().
  5. init() is one-shot. See Methods → init is one-shot.

Watching the wire#

Open the emulator, point it at any game URL, and the Bridge traffic panel logs every byte. Toggle the verbose switch and each row gets a plain-English annotation. That’s the fastest way to internalize the model.

Common pitfalls#

  • “My events don’t fire” — your game has not called init() yet, or has not yet awaited it.
  • “My save doesn’t persist” — you forgot to declare the saveState capability in init({ capabilities: [...] }).
  • init timed out” — the platform did not respond in 30 s. Confirm the game is loaded inside a real GAMEE platform, or use the emulator / MockBridge.
  • “Random fields are missing in init data” — the platform only sends fields that are relevant. missionData is only present when the user opened the game from a mission card; saveState is only present if a save exists.