Spaces:
Running on Zero
Running on Zero
Download avatar/turn-loop.js from WolfDavid/japanese-learning-avatar: direct link, hf CLI and curl.
- Browser
- Download file 2.75 kB
-
https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar/resolve/f8cc6091b2ae6d7c8fde0f73c6a462b10fa47310/avatar/turn-loop.js
- Command line
-
hf download hf://spaces/WolfDavid/japanese-learning-avatar@f8cc6091b2ae6d7c8fde0f73c6a462b10fa47310/avatar/turn-loop.js
-
curl -L -o turn-loop.js https://huggingface.co/spaces/WolfDavid/japanese-learning-avatar/resolve/f8cc6091b2ae6d7c8fde0f73c6a462b10fa47310/avatar/turn-loop.js
2.75 kB
| // THIS MODULE IS TRANSPORT-AGNOSTIC AND IS THE ONLY IMPLEMENTATION OF THE TURN LOOP. | |
| // Both avatar.js (inline gr.HTML) and avatar-iframe.js (postMessage) import it. | |
| // Do NOT add turn behaviour to either transport file - add it here, or the iframe | |
| // fallback silently loses the feature. tests/test_transport_seam.py enforces this. | |
| // | |
| // The renderer is reachable only through the six-method stagePort. The host bridge is | |
| // reachable only through getServer(), which is a GETTER rather than a value so this | |
| // module never holds a host object and can be constructed before the bridge exists. | |
| /** | |
| * A deferred method. Calling one now fails loudly and names the plan that fills it in, | |
| * which is the difference between a placeholder and an accidental no-op. The parity | |
| * guard is therefore meaningful from this wave rather than only after the last one. | |
| */ | |
| function notWiredYet(name, plan) { | |
| return () => { | |
| throw new Error(`Avatar.${name}() is not wired yet - plan ${plan} implements it`); | |
| }; | |
| } | |
| /** | |
| * @param {object} opts | |
| * @param {object} opts.stagePort the renderer boundary | |
| * @param {Function} [opts.emit] the facade's event bus | |
| * @param {Function} [opts.getServer] returns the host bridge, or null when there is none | |
| */ | |
| export function createTurnLoop({ stagePort, emit = () => {}, getServer = () => null } = {}) { | |
| if (!stagePort) throw new Error('createTurnLoop: a stagePort is required'); | |
| // The turn loop's own slice of __debug. The facade merges this object; it does not | |
| // own it, and this module does not own the facade's. | |
| const state = { | |
| thinking: false, | |
| listening: false, | |
| lastTurnId: null, | |
| replayCount: 0, | |
| }; | |
| return { | |
| state, | |
| getServer, | |
| setThinking(value) { | |
| const on = !!value; | |
| state.thinking = on; | |
| stagePort.setThinking(on); | |
| return on; | |
| }, | |
| setListening(value) { | |
| const on = !!value; | |
| state.listening = on; | |
| stagePort.setListening(on); | |
| return on; | |
| }, | |
| /** | |
| * Re-play the cached directive. There is deliberately no network access of any | |
| * kind in here: plan 01-09's test_replay asserts zero requests with a browser | |
| * request listener, and a cache miss must fail rather than quietly re-download. | |
| */ | |
| async replay() { | |
| state.replayCount += 1; | |
| try { | |
| return await stagePort.replayCached(); | |
| } catch (err) { | |
| emit('error', { message: String(err?.message ?? err), where: 'replay' }); | |
| throw err; | |
| } | |
| }, | |
| startListening: notWiredYet('startListening', '01-07'), | |
| stopListening: notWiredYet('stopListening', '01-07'), | |
| dispatchTurn: notWiredYet('dispatchTurn', '01-08'), | |
| requestSlower: notWiredYet('requestSlower', '01-08'), | |
| }; | |
| } | |