File size: 2,746 Bytes
7146e75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 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'),
  };
}