Architecture

This document is currently available in Chinese only.

本文档面向平台内部与深度接入方,内容可能随实现演进。

本文说明 SDK 的定位、分层、运行模式、数据流、存储模型与演进接缝,结论以 SDK 1.2.0 为准。快速接入见「快速开始」,方法签名见「API 速查」。

定位与设计目标

SDK 让 H5 游戏在不关心运行环境差异的前提下,用同一套 API 获得平台能力(账号、钱包、存档、榜单、埋点、广告、游戏级存储)。核心设计目标:

- StorageDriver:键值持久化,现为 localStorage / 内存; - HostAdapter:宿主能力查询与广告播放,现为 android / web / browser; - Transport:HTTP 请求,现为基于 fetch 的实现。

  1. 本地优先(local-first),按模块区分。本地模式(含 auto 回落)下读写全部以本地存储为准(localStorage,不可用时降级内存),断网、无后端时游戏仍可完整运行;远端模式下存档仍先落本地、再同步服务端,而钱包与广告奖励直接读写服务端,读取以服务端为准,仅 NETWORK 时回退本地(目前为排行榜与存档缓存)。
  2. 后端形状 API(backend-shaped API)。模块按未来后端的资源边界划分:account / wallet / game / leaderboard / analytics / ads / storage。本地实现先以相同形状落地,接入远端时游戏代码零改动。
  3. 两个接缝加传输层。把设备与浏览器差异收敛到三个可替换抽象:
  4. 未来换后端不改游戏代码。游戏只依赖 types 契约,运行时由 SDK 装配层决定注入本地实现还是远端实现。

非目标(1.x 不做):实时多人、支付、好友关系、真实广告 SSV 与反作弊。

分层架构

层职责
契约层(types)纯类型:配置、各模块接口与数据结构,可被零成本引用
门面层(Instfun 单例)模式解析、依赖装配、本地/远端工厂与单例管理
本地模块层(modules)account / wallet / game(编排会话、记录、统计)/ progress(存档)/ leaderboard / analytics / ads / game-storage
远端模块层(remote)identity、wallet、game(包装本地 game + 同步)、outbox、cache、leaderboard(fallback 本机榜)、analytics、ads、migrate
接缝层StorageDriver(LocalStorage / Memory)、HostAdapter(Android / Web / Browser)、Transport(Fetch)
底座(core)config 常量、errors 错误模型、ids 标识符、meta 平台元信息、watermark 页面水印、url-locale

依赖方向单向:core ← storage ← modules ← sdk,以及 core / storage / host ← remote ← sdk。modules 不感知 remote;remote 也不直接引用 modules,本地实例由装配层在运行期作为依赖注入(如远端 game 包装本地 game、远端 analytics 包装本地队列)。装配层是唯一同时引用两层的代码。

运行模式

mode 支持 local / remote / auto;不传时按 auto 处理,但网络错误回落只认显式 mode: "auto"。初始化后 sdk.mode 只会是 local 或 remote。

基础 URL 解析优先级:

顺序条件结果
1mode: "local"null(纯本地,忽略 apiBaseUrl)
2config.apiBaseUrl 非空使用它
3window.INSTFUN_CONFIG.apiBaseUrl 非空(门户注入)使用它
4mode: "remote"默认 "/api/v1"(同源部署)
5其余(auto 且未配置)null,本地模式

auto 的回落规则:初始化先尝试远端;只有 mode: "auto"、错误是 InstfunError 且错误码为 NETWORK 时才回落本地。配置错误、鉴权失败等会原样抛给调用方,显式 remote 不做任何回落,避免把真实问题静默成单机模式。

初始化成功后统一记录 sdk_init 埋点,属性为 { mode, host, persistent }。

模块关系与依赖方向

本地层:

  • wallet、analytics、leaderboard 只依赖存储与底座;
  • game 是编排层:持有存档存储、analytics、leaderboard 引用,负责会话/记录/统计,并在 finish 时把存档与榜单作为可选副作用调用;
  • ads 依赖宿主适配器、钱包模块与埋点模块;
  • account 的身份结果由装配层以 ensureUserId() 注入其它模块,保证同一进程内 userId 稳定。

远端层(包装而非重写):

  • account、wallet、leaderboard、analytics、ads 都实现与本地同名模块一致的接口;
  • 远端 game 组合本地 game:先本地落盘,再远端同步;本地模块同时充当离线缓存与统计来源;
  • 远端 analytics 的 track / getQueue / clear 直接转发本地实现,只重写 flush()(分片上报);
  • 远端 ads 在宿主为浏览器时直接返回本地广告模块(模拟广告没有服务端票据可核销),其余宿主走「服务端领 token → 播放 → 核销」;
  • 远端 leaderboard 的 fallback 指向本地排行榜,仅 NETWORK 时回退;
  • outbox 与远端 game 共享同一套存档载荷 helper;outbox 与远端 game 之间存在模块级循环引用,依赖函数声明提升与延迟调用化解;
  • 存档缓存直接复用本地存档键,使模式切换(如 auto 回落)时不会出现两份存档。

依赖方向:core / storage / host 是底座,modules 与 remote 同层,装配层是唯一同时引用两层的文件;types 可被任意层 import type。

数据流

初始化与身份引导:

- 内存资料命中则直接返回; - 有 token 时调用 GET /me 校验并获取最新资料,401 则删除 token 后用 deviceSeed 重新游客登录; - 无 token 时用 POST /auth/guest { deviceSeed } 换回同一游客账号。

  1. 解析 baseUrl;无 baseUrl 时直接构建纯本地 SDK。
  2. 有 baseUrl 时先装配远端模块(不发请求),再执行 bootstrap:
  3. 首次远端启动执行本地存档一次性迁移:本地有存档且服务端无存档时上传一次(带迁移标记,避免重复)。
  4. 记录 sdk_init 埋点并返回 SDK 实例。

deviceSeed 是 32 字节随机十六进制串,一旦生成不再变化,是「同一设备同一账号」的锚点;token 失效只会重新游客登录,不会换号。

游玩主链路:

  • loadProgress():请求服务端存档,成功则写回本地缓存;仅 NETWORK 时读本地缓存并返回 pendingSync: true。
  • start():本地创建会话、更新平台元信息并埋点;发现上次遗留的未结束会话时按 quit 补记一条。
  • saveProgress():本地先落盘(revision + 1、256KB 校验),再按节流窗口决定直推还是合并进 outbox;直推冲突时拉取服务端存档、以服务端为底做浅合并后重试一次,二次冲突进 outbox;NETWORK / RATE_LIMITED 同样进 outbox 并返回 pendingSync: true,其它错误上抛。
  • finish():先本地落盘记录与统计,再提交记录(NETWORK / RATE_LIMITED 进 outbox),随后在存在 progress 时推送存档,最后在 submitScore 且分数合法时提交榜单并拉取快照。

sync 与离线补偿:

  1. 先 flush 埋点队列:按 100 条分批上传,全部成功才清空;中途失败回写未发送尾部。
  2. 再按入队顺序重放 outbox:遇到第一个可重试失败(NETWORK / SERVER_ERROR / RATE_LIMITED)即停止并保留该操作及其后所有操作;不可重试错误移除该操作并上报 outbox_op_dropped 埋点;存档二次冲突保留在队列中但不阻塞后续操作。
  3. 返回 { flushed, pending },本地模式下不发网络请求,只返回埋点积压数。

存储模型

所有键都以 instfun:v1 开头;远端专用键在 v1 前缀下嵌入 :v2: 段,与本地键空间隔离。

键结构读写方
instfun:v1:identity身份记录account
instfun:v1:metaPlatformMeta { games, recentGameIds }平台元信息
instfun:v1:wallet:balance余额快照wallet
instfun:v1:wallet:transactions流水数组(≤ 200 条)wallet
instfun:v1:analytics:queue事件队列(≤ 500 条)analytics
instfun:v1:game:{gameId}:progress云存档(含 revision)progress / 存档缓存
instfun:v1:game:{gameId}:records记录数组(≤ 50 条)game
instfun:v1:game:{gameId}:stats游戏统计game
instfun:v1:game:{gameId}:session未结束会话game
instfun:v1:game:{gameId}:boards按 boardId 的本机榜记录leaderboard
instfun:v1:game:{gameId}:kv:{key}任意可 JSON 化值game-storage
instfun:v1:v2:device-seed32 字节 hex 字符串(裸键)identity
instfun:v1:v2:tokentoken 字符串(裸键)identity
instfun:v1:v2:session{ userId, profile }(信封)identity
instfun:v1:v2:outbox离线操作队列(≤ 200 条)outbox
instfun:v1:v2:migrated:{gameId}迁移标记(裸键)migrate

「信封」是本地持久化的统一结构:

{ "schemaVersion": 1, "updatedAt": 1700000000000, "value": "..." }

读取时若 JSON 损坏、缺字段或 schemaVersion 不匹配,一律按「无数据」返回 null。device-seed、token、recovery-code 与迁移标记为裸字符串,不经信封,因此 schema 升级不会使它们失效。

游戏级 KV 的前缀为 instfun:v1:game:{gameId}:kv:,keys() 按前缀过滤并裁掉前缀,游戏只看到自己的 key;不同游戏即使 key 同名也互不影响。

安全与配额

SDK 侧本地校验:

项阈值行为
存档 JSON 序列化长度256KB写盘前抛 PAYLOAD_TOO_LARGE
单条事件 properties 长度8KB丢弃该事件(不入队、不抛错)
单次进程埋点入队上限1000 条(可配置)超出丢弃
埋点队列长度500 条保留最新
钱包流水条数200 条保留最新
游玩记录条数50 条保留最新
单次会话时长6 小时封顶
单笔金额[1, 10000] 整数非法抛 INVALID_AMOUNT
远端存档推送节流2000ms(可配置)窗口内走 outbox 合并

远端错误处理:

  • 429 RATE_LIMITED:details 可含 retryAfterSeconds / limit / window。存档、记录、榜单在 outbox 中视为可重试,延迟到下次 sync();钱包与广告奖励原样抛给游戏,不应自动重试。
  • 413 PAYLOAD_TOO_LARGE:即使响应无 JSON 信封也按 HTTP 状态兜底映射;本地已在 256KB 处拦截存档。
  • Retry-After:服务端补充该响应头,SDK 不解析,时间语义以 details.retryAfterSeconds 为准。

水印:在官网 play/{gameId}/ 内运行时,SDK 读取页面水印 <meta name="instfun-wm">(trim 后 1~64 字符有效,进程内缓存一次),埋点未自带 wm 时自动附加 properties.wm;游戏显式传入的优先。水印仅用于内容溯源,不参与鉴权。

版本与兼容

  • SDK_VERSION = "1.2.0",随包导出,同时写入埋点事件、云存档与游戏元信息;
  • 云存档另带 gameVersion,埋点事件带 gameId / gameVersion,便于按版本切分数据;
  • 存档信封的 schemaVersion 当前为 1:读取时版本不一致即返回 null。升级该值等于让旧本地数据失效,必须配套迁移方案;
  • 兼容原则:只做加法变更(新增可选字段、模块、宿主能力),不修改既有字段语义与错误码含义;
  • 迁移只针对旧版存档:远端首次启动时把本地存档上传一次(仅服务端无存档时),并打标避免重复。

演进接缝

  1. 传输层已就绪:Transport 只要求 request<T>({ method, path, body });默认实现已处理信封解码、token 注入与错误归一。自定义实现或代理可通过初始化依赖注入。
  2. 本地到远端的模块替换点:集中在装配层。替换某个能力时只需实现对应的远端模块,在装配时替换工厂调用;本地模块保持原样供离线兜底。
  3. 新的存储后端:StorageDriver 的方法全部返回 Promise,天然适配异步后端。新增 IndexedDB 实现时只需实现该接口并在存储探测中增加分支,业务代码无需改动。
  4. 新的宿主:实现 HostAdapter 后增加探测分支与 HostKind 成员;广告是否走服务端票据由远端广告模块按宿主类型判断。
  5. 服务端协议演进:远端调用只依赖统一信封 { ok, data } / { ok: false, error: { code, message, details } };新增端点只需替换远端模块中的 path,错误码经映射表收敛到 InstfunErrorCode。

术语表

术语说明
门面(facade)负责模式解析、依赖装配、工厂与单例的入口层,游戏侧唯一接触点
接缝(seam)把设备与浏览器差异收敛成的三个可替换抽象:StorageDriver、HostAdapter、Transport
local-first本地模式(含 auto 回落)下读写以本地存储为准,断网时游戏仍可完整运行
backend-shaped API模块按后端资源边界划分,本地与远端实现同形,接入远端时游戏代码零改动
信封(envelope)本地持久化的统一结构 { schemaVersion, updatedAt, value }
outbox远端模式下失败写操作(存档/记录/榜单)的持久化补偿队列,sync() 时按序重放
fallback读取路径仅在 NETWORK 错误时回退本地数据,其它错误上抛
bootstrap初始化时执行一次的流程:deviceSeed 游客登录与本地存档一次性迁移
水印 wm官网游戏页通过 meta 注入的溯源标记,埋点未自带 wm 时自动附加
宿主(host)承载 H5 游戏的环境抽象(android / web / browser)
TransportHTTP 请求抽象,负责信封解码与 token 注入
revision云存档的单调版本号,推送时以 revision - 1 做乐观并发控制