franke-arcade-sdk.js aktualisiert
This commit is contained in:
+180
-15
@@ -1,21 +1,51 @@
|
||||
const FrankieSDK = (() => {
|
||||
const _handlers = {}
|
||||
const _llmResolvers = {}
|
||||
const _storageResolvers = {}
|
||||
|
||||
// _handle dispatches an inbound postMessage event to the registered hook.
|
||||
// Dev mock mode: active when the game runs outside a Frankie host frame (opened
|
||||
// standalone) or when explicitly requested with ?frankie_dev=1. In mock mode the SDK
|
||||
// emulates the host locally instead of posting to a parent frame.
|
||||
let _mock = window.parent === window || /[?&]frankie_dev=1\b/.test(window.location.search)
|
||||
let _gotInit = false
|
||||
let _declarationCache // Promise resolving to the manifest storage declaration (or null)
|
||||
|
||||
const MOCK_INIT = { userId: 0, username: 'DevPlayer', gameId: 'dev', theme: 'dark' }
|
||||
|
||||
// _handle dispatches an inbound event to the registered lifecycle hook.
|
||||
function _handle(type, payload) {
|
||||
const hook = _handlers[type]
|
||||
if (typeof hook === 'function') hook(payload)
|
||||
}
|
||||
|
||||
// Boot: listen for messages from the Frankie parent frame.
|
||||
// _send routes an outbound message: to the real parent frame, or to the local mock host.
|
||||
function _send(message) {
|
||||
if (_mock) {
|
||||
_mockHandle(message)
|
||||
return
|
||||
}
|
||||
window.parent.postMessage(message, '*')
|
||||
}
|
||||
|
||||
// _settleStorage resolves or rejects the pending storage Promise for a request id.
|
||||
function _settleStorage(id, result) {
|
||||
const pending = _storageResolvers[id]
|
||||
if (!pending) return
|
||||
delete _storageResolvers[id]
|
||||
if (result && result.ok) pending.resolve(result.value)
|
||||
else pending.reject(new Error(result?.error || 'storage error'))
|
||||
}
|
||||
|
||||
// Boot: listen for messages from the Frankie parent frame (ignored in mock mode).
|
||||
window.addEventListener('message', (event) => {
|
||||
const { type, payload, id } = event.data || {}
|
||||
if (typeof type !== 'string' || !type.startsWith('FRANKIE_')) return
|
||||
|
||||
const hook = type.slice(8).toLowerCase() // FRANKIE_INIT → 'init'
|
||||
|
||||
// LLM responses are resolved as promises rather than dispatched as hooks.
|
||||
if (hook === 'init') _gotInit = true
|
||||
|
||||
// LLM + storage responses resolve Promises rather than dispatching hooks.
|
||||
if (hook === 'llm_response') {
|
||||
const resolve = _llmResolvers[id]
|
||||
if (typeof resolve === 'function') {
|
||||
@@ -24,38 +54,173 @@ const FrankieSDK = (() => {
|
||||
}
|
||||
return
|
||||
}
|
||||
if (hook === 'storage_result') {
|
||||
_settleStorage(id, payload)
|
||||
return
|
||||
}
|
||||
|
||||
_handle(hook, payload)
|
||||
})
|
||||
|
||||
return {
|
||||
// In mock mode, deliver a synthetic init once the game has registered its handlers
|
||||
// (games call sdk.on('init', …) synchronously on load, so a 0ms timer finds them).
|
||||
if (_mock) {
|
||||
setTimeout(() => _handle('init', MOCK_INIT), 0)
|
||||
} else {
|
||||
// Iframed but possibly not under a real Frankie host: if no init arrives shortly,
|
||||
// fall back to mock mode so the game still boots during local testing.
|
||||
setTimeout(() => {
|
||||
if (!_gotInit) {
|
||||
_mock = true
|
||||
_handle('init', MOCK_INIT)
|
||||
}
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const api = {
|
||||
// on registers a lifecycle hook. Returns the SDK for chaining.
|
||||
on(event, handler) {
|
||||
_handlers[event] = handler
|
||||
return this
|
||||
},
|
||||
|
||||
// emit sends a typed event to the Frankie parent frame.
|
||||
// emit sends a typed event to Frankie (or logs it in mock mode).
|
||||
emit(event, payload = {}) {
|
||||
window.parent.postMessage(
|
||||
{ type: `FRANKIE_${event.toUpperCase()}`, payload },
|
||||
'*'
|
||||
)
|
||||
_send({ type: `FRANKIE_${event.toUpperCase()}`, payload })
|
||||
},
|
||||
|
||||
// llm sends a chat-completion request to Frankie's LLM and returns a Promise
|
||||
// that resolves with the model's reply as a string.
|
||||
// llm sends a chat-completion request and resolves with the model's reply.
|
||||
llm(messages) {
|
||||
return new Promise((resolve) => {
|
||||
const id = `llm_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
||||
_llmResolvers[id] = resolve
|
||||
window.parent.postMessage(
|
||||
{ type: 'FRANKIE_LLM_REQUEST', id, payload: { messages } },
|
||||
'*'
|
||||
)
|
||||
_send({ type: 'FRANKIE_LLM_REQUEST', id, payload: { messages } })
|
||||
})
|
||||
},
|
||||
|
||||
// storage is the only sanctioned way to persist game data. Both methods return a
|
||||
// Promise; get resolves with the stored object, set with the persisted object.
|
||||
storage: {
|
||||
get(store = 'default') {
|
||||
return _requestStorage('FRANKIE_STORAGE_GET', { store })
|
||||
},
|
||||
set(store, value) {
|
||||
return _requestStorage('FRANKIE_STORAGE_SET', { store, value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _requestStorage issues a storage request and returns a Promise settled by the host
|
||||
// (real or mock) via a FRANKIE_STORAGE_RESULT reply.
|
||||
function _requestStorage(type, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = `st_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
||||
_storageResolvers[id] = { resolve, reject }
|
||||
_send({ type, id, payload })
|
||||
})
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Mock host (dev mode) — a self-contained emulation of Frankie's bridge so a
|
||||
// game can be exercised standalone. It mirrors the storage validation rules.
|
||||
// ==========================================================================
|
||||
|
||||
function _mockHandle(message) {
|
||||
const { type, id, payload } = message
|
||||
switch (type) {
|
||||
case 'FRANKIE_READY':
|
||||
case 'FRANKIE_XP':
|
||||
case 'FRANKIE_WIN':
|
||||
case 'FRANKIE_SCORE':
|
||||
case 'FRANKIE_CLOSE':
|
||||
console.log(`[FrankieSDK dev] ${type}`, payload || {})
|
||||
break
|
||||
case 'FRANKIE_LLM_REQUEST': {
|
||||
const resolve = _llmResolvers[id]
|
||||
if (typeof resolve === 'function') {
|
||||
resolve('[FrankieSDK dev] LLM is stubbed in standalone mode.')
|
||||
delete _llmResolvers[id]
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'FRANKIE_STORAGE_GET':
|
||||
_mockStorage('read', payload).then((result) => _settleStorage(id, result))
|
||||
break
|
||||
case 'FRANKIE_STORAGE_SET':
|
||||
_mockStorage('write', payload).then((result) => _settleStorage(id, result))
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// _mockDeclaration fetches and caches the game's own manifest storage block.
|
||||
function _mockDeclaration() {
|
||||
if (!_declarationCache) {
|
||||
_declarationCache = fetch('./frankie-game.json')
|
||||
.then((res) => (res.ok ? res.json() : {}))
|
||||
.then((manifest) => manifest?.storage || null)
|
||||
.catch(() => null)
|
||||
}
|
||||
return _declarationCache
|
||||
}
|
||||
|
||||
async function _mockStorage(op, payload) {
|
||||
const storeName = payload?.store || 'default'
|
||||
try {
|
||||
const decl = await _mockDeclaration()
|
||||
const store = decl?.stores?.[storeName]
|
||||
if (!store) throw new Error(`store "${storeName}" is not declared by this game`)
|
||||
_mockAssertAccess(store, op)
|
||||
|
||||
const key = `frankie_dev_gamedata_${storeName}`
|
||||
if (op === 'read') {
|
||||
return { ok: true, value: JSON.parse(localStorage.getItem(key) || '{}') }
|
||||
}
|
||||
_mockValidate(store.fields, payload?.value)
|
||||
localStorage.setItem(key, JSON.stringify(payload.value))
|
||||
return { ok: true, value: payload.value }
|
||||
} catch (err) {
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
function _mockAssertAccess(store, op) {
|
||||
const a = store.access
|
||||
if (op === 'read' && (a === 'read' || a === 'readwrite')) return
|
||||
if (op === 'write' && (a === 'write' || a === 'readwrite')) return
|
||||
throw new Error(op === 'read' ? 'this store is write-only' : 'this store is read-only')
|
||||
}
|
||||
|
||||
function _mockValidate(fields, value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error('value must be an object')
|
||||
}
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
if (!(key in fields)) throw new Error(`unknown field "${key}"`)
|
||||
_mockCheck(key, fields[key], val)
|
||||
}
|
||||
}
|
||||
|
||||
function _mockCheck(name, spec, val) {
|
||||
if (typeof spec === 'string') {
|
||||
const ok =
|
||||
(spec === 'string' && typeof val === 'string') ||
|
||||
(spec === 'number' && typeof val === 'number') ||
|
||||
(spec === 'boolean' && typeof val === 'boolean') ||
|
||||
(spec === 'object' && val && typeof val === 'object' && !Array.isArray(val)) ||
|
||||
(spec === 'string[]' && Array.isArray(val) && val.every((v) => typeof v === 'string')) ||
|
||||
(spec === 'number[]' && Array.isArray(val) && val.every((v) => typeof v === 'number'))
|
||||
if (!ok) throw new Error(`field "${name}" must be a ${spec}`)
|
||||
} else {
|
||||
if (!val || typeof val !== 'object' || Array.isArray(val)) {
|
||||
throw new Error(`field "${name}" must be an object`)
|
||||
}
|
||||
_mockValidate(spec, val)
|
||||
}
|
||||
}
|
||||
|
||||
return api
|
||||
})()
|
||||
|
||||
export default FrankieSDK
|
||||
Reference in New Issue
Block a user