Download lib/api.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/api.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/api.js
-
curl -L -o api.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/api.js
10.4 kB
| // window.unroll: the stable public API for people and agents. The page supplies the implementations; | |
| // this file adds argument checking, uniform JSON results, events, batches and waiting. | |
| import { METHODS, EVENTS, URL_PARAMS, KEYS, API_VERSION, checkArgs } from './apispec.js'; | |
| export class ApiError extends Error { constructor(message, code = 'invalid') { super(message); this.code = code; } } | |
| const json = (v) => JSON.parse(JSON.stringify(v ?? null)); | |
| const get = (o, path) => path.split('.').reduce((a, k) => (a == null ? undefined : a[k]), o); | |
| export function installAPI(impl, { stateFn, onTick }) { | |
| const subs = new Map(); | |
| let nextId = 1; | |
| const emit = (event, payload) => { | |
| for (const [, s] of subs) if (s.event === event) { try { s.cb(json(payload)); } catch (e) { console.warn('unroll event handler', e); } } | |
| }; | |
| const api = {}; | |
| const ext = {}; // name -> { group, summary, params, fn } for add-ons such as lib/bench.js | |
| const wrap = (name) => async (...args) => { | |
| const bad = ext[name] ? null : checkArgs(name, args); | |
| if (bad) { emit('error', { method: name, error: bad }); return { ok: false, error: bad, code: 'invalid' }; } | |
| try { | |
| const out = await (ext[name] ? ext[name].fn : impl[name])(...args); | |
| return { ok: true, ...(out && typeof out === 'object' && !Array.isArray(out) ? json(out) : { result: json(out) }) }; | |
| } catch (e) { | |
| const error = e && e.message ? e.message : String(e); | |
| emit('error', { method: name, error }); | |
| return { ok: false, error, code: e.code || 'failed' }; | |
| } | |
| }; | |
| // meta methods that live here | |
| impl.schema = async () => ({ | |
| version: API_VERSION, app: window.UNROLL_VERSION || null, | |
| methods: Object.fromEntries([...Object.entries(METHODS), ...Object.entries(ext)].map(([k, m]) => [k, { group: m.group, summary: m.summary, params: m.params, ...(ext[k] ? { extension: true } : {}) }])), | |
| extensions: [...new Set(Object.values(ext).map((m) => m.group))], | |
| events: EVENTS, urlParams: URL_PARAMS, keys: KEYS, | |
| conventions: 'Every method returns a Promise of { ok: true, ... } or { ok: false, error, code }. Nothing throws. Screen coordinates are CSS pixels; scene coordinates are millimeters. Add-ons register more groups with unroll.register(group, methods, specs); they appear here with extension: true.', | |
| }); | |
| // help(): a short guide for agents; help('setLayer') or help('destroy') narrows it to a method or a group | |
| impl.help = async (topic) => { | |
| const all = { ...METHODS, ...ext }; | |
| const fmtM = (k, m) => ({ method: k, group: m.group, summary: m.summary, params: (m.params || []).map((p) => `${p.name}${p.required === false ? '?' : ''}: ${p.type}${p.desc ? ` (${p.desc})` : ''}`), call: `unroll.${k}(${(m.params || []).map((p) => p.name).join(', ')})` }); | |
| if (topic !== undefined) { | |
| if (typeof topic !== 'string') throw new ApiError('help: topic must be a method name (e.g. "setLayer") or a group (e.g. "destroy").'); | |
| if (all[topic]) return fmtM(topic, all[topic]); | |
| const g = Object.entries(all).filter(([, m]) => m.group === topic); | |
| if (g.length) return { group: topic, methods: g.map(([k, m]) => fmtM(k, m)) }; | |
| throw new ApiError(`help: no method or group "${topic}". Groups: ${[...new Set(Object.values(all).map((m) => m.group))].join(', ')}.`, 'not_found'); | |
| } | |
| return { | |
| about: 'window.unroll drives every control of the Unroll scroll viewer. Every call returns a Promise of { ok: true, ... } or { ok: false, error, code }; nothing throws.', | |
| start: ['await unroll.waitFor("ready")', 'await unroll.getState()', 'await unroll.setUnroll(0.5)', 'await unroll.setLayer("overlay")', 'await unroll.screenshot()'], | |
| groups: Object.fromEntries([...new Set(Object.values(all).map((m) => m.group))].map((g) => [g, Object.keys(all).filter((k) => all[k].group === g)])), | |
| more: 'unroll.help("destroy") for a group, unroll.help("setLayer") for one method, unroll.schema() for everything as JSON. Docs: /AGENTS.md, /llms.txt, /api.json, /docs/EMBED.md.', | |
| version: window.UNROLL_VERSION || null, | |
| }; | |
| }; | |
| impl.on = (event, cb) => { | |
| if (typeof cb !== 'function') throw new ApiError('on: callback must be a function.'); | |
| const id = nextId++; subs.set(id, { event, cb }); return { id }; | |
| }; | |
| impl.off = (id) => { if (!subs.delete(id)) throw new ApiError(`off: no subscription with id ${id}.`, 'not_found'); return {}; }; | |
| impl.run = async (steps, options = {}) => { | |
| const results = []; | |
| for (const [i, s] of steps.entries()) { | |
| const method = Array.isArray(s) ? s[0] : s && s.method; | |
| const args = Array.isArray(s) ? s.slice(1) : (s && s.args) || []; | |
| if (typeof method !== 'string' || !fns[method] || method === 'run') { results.push({ ok: false, error: `Step ${i}: unknown method ${JSON.stringify(method)}.`, code: 'invalid' }); if (!options.continueOnError) break; continue; } | |
| const r = await call(method, args); | |
| results.push({ method, ...r }); | |
| if (!r.ok && !options.continueOnError) break; | |
| } | |
| return { results, completed: results.filter((r) => r.ok).length, total: steps.length }; | |
| }; | |
| impl.waitFor = (cond, options = {}) => new Promise((resolve, reject) => { | |
| const timeout = options.timeout ?? 10000; | |
| if (typeof cond === 'string' && !['ready', 'settled', 'overlayLoaded'].includes(cond)) { reject(new ApiError(`waitFor: unknown condition "${cond}". Use "ready", "settled", "overlayLoaded" or an object.`)); return; } | |
| const test = (st) => { | |
| if (cond === 'ready') return st.ready; | |
| if (cond === 'settled') return st.settled; | |
| if (cond === 'overlayLoaded') return st.layers.overlayLoaded; | |
| return Object.entries(cond).every(([path, want]) => { | |
| const v = get(st, path); | |
| if (want && typeof want === 'object' && !Array.isArray(want)) { | |
| return Object.entries(want).every(([op, x]) => (op === 'eq' ? v === x : op === 'ne' ? v !== x : op === 'gte' ? v >= x : op === 'lte' ? v <= x : op === 'gt' ? v > x : op === 'lt' ? v < x : op === 'in' ? Array.isArray(x) && x.includes(v) : false)); | |
| } | |
| return JSON.stringify(v) === JSON.stringify(want); | |
| }); | |
| }; | |
| const t0 = performance.now(); | |
| const tick = () => { | |
| const st = stateFn(); | |
| if (test(st)) { resolve({ state: st, ms: Math.round(performance.now() - t0) }); return; } | |
| if (performance.now() - t0 > timeout) { reject(new ApiError(`waitFor timed out after ${timeout} ms.`, 'timeout')); return; } | |
| setTimeout(tick, 50); | |
| }; | |
| tick(); | |
| }); | |
| const call = (name, args) => (fns[name] ? fns[name](...args) : Promise.resolve({ ok: false, error: `Unknown method "${name}".`, code: 'invalid' })); | |
| const fns = {}; | |
| for (const name of Object.keys(METHODS)) { | |
| if (!impl[name]) throw new Error(`API method ${name} has no implementation`); | |
| fns[name] = wrap(name); | |
| const [g, m] = name.split('.'); | |
| if (m) { api[g] = api[g] || {}; api[g][m] = fns[name]; } else api[g] = fns[name]; | |
| } | |
| // state change events, checked a few times a second | |
| let last = null, lastT = null, wasSettled = false; | |
| setInterval(() => { | |
| const st = stateFn(); | |
| const cmp = { layer: st.layer, focus: st.focus, panels: st.panels, destroy: { on: st.destroy.on, tool: st.destroy.tool, sandbox: st.destroy.sandbox }, scroll: st.scroll.key }; | |
| if (last) { | |
| const changed = Object.keys(cmp).filter((k) => JSON.stringify(cmp[k]) !== JSON.stringify(last[k])); | |
| if (changed.length) emit('change', { changed, state: st }); | |
| if (changed.includes('layer')) emit('layer', { layer: st.layer }); | |
| if (changed.includes('focus')) emit('focus', { focus: st.focus }); | |
| if (changed.includes('panels')) emit('panel', { panels: st.panels }); | |
| if (changed.includes('destroy')) emit('destroy', { destroy: st.destroy }); | |
| } | |
| if (st.settled && !wasSettled) { | |
| emit('settled', { state: st }); | |
| if (lastT === null || Math.abs(st.t - lastT) > 1e-4) { emit('unroll', { t: st.t }); emit('change', { changed: ['t'], state: st }); } | |
| emit('camera', { camera: st.camera }); | |
| lastT = st.t; | |
| } | |
| wasSettled = st.settled; | |
| last = json(cmp); | |
| if (onTick) onTick(st); | |
| }, 200); | |
| // Extensions: a script registers a new group (for example window.unroll.bench) with | |
| // window.unroll.register('bench', { run: fn, ... }, { run: { summary, params } }). | |
| // Scripts that load before the API can push the same arguments onto window.unrollExtensions. | |
| const reserved = new Set(['version', 'register', ...Object.keys(api)]); | |
| const register = (group, methods, specs = {}) => { | |
| if (typeof group !== 'string' || !/^[a-z][a-zA-Z0-9]*$/.test(group)) return { ok: false, error: 'register: group must be a simple lower camel case name.', code: 'invalid' }; | |
| if (reserved.has(group)) return { ok: false, error: `register: "${group}" is already taken.`, code: 'conflict' }; | |
| if (!methods || typeof methods !== 'object' || !Object.keys(methods).length) return { ok: false, error: 'register: methods must be an object of functions.', code: 'invalid' }; | |
| const ns = {}; | |
| for (const [m, fn] of Object.entries(methods)) { | |
| if (typeof fn !== 'function') return { ok: false, error: `register: ${group}.${m} is not a function.`, code: 'invalid' }; | |
| const name = `${group}.${m}`, sp = specs[m] || {}; | |
| ext[name] = { group, summary: sp.summary || '', params: sp.params || [], fn }; | |
| fns[name] = wrap(name); ns[m] = fns[name]; | |
| } | |
| reserved.add(group); | |
| Object.defineProperty(api, group, { value: Object.freeze(ns), enumerable: true, writable: false, configurable: false }); | |
| emit('extension', { group, methods: Object.keys(ns) }); | |
| return { ok: true, group, methods: Object.keys(ns) }; | |
| }; | |
| for (const k of Object.keys(api)) { | |
| if (api[k] && typeof api[k] === 'object') Object.freeze(api[k]); | |
| Object.defineProperty(api, k, { writable: false, configurable: false }); | |
| } | |
| Object.defineProperty(api, 'register', { value: register, enumerable: true, writable: false, configurable: false }); | |
| Object.defineProperty(window, 'unroll', { value: api, configurable: false, writable: false }); | |
| const queued = Array.isArray(window.unrollExtensions) ? window.unrollExtensions : []; | |
| window.unrollExtensions = { push: (...items) => { for (const it of items) register(...it); return 0; } }; | |
| for (const it of queued) register(...it); | |
| return { api, emit, call }; | |
| } | |