function convertValToSlot(val) {
    if (!valToSlot.has(val)) {
      /** @type {CapTPSlot} */
      let slot;
      if (exportedTrapHandlers.has(val)) {
        lastTrapID += 1;
        slot = `t+${lastTrapID}`;
      } else {
        slot = importExportTables.makeSlotForValue(val);
      }
      if (exportHook) {
        exportHook(val, slot);
      }
      if (isPromise(val)) {
        // Set up promise listener to inform other side when this promise
        // is fulfilled/broken
        const promiseID = reverseSlot(slot);
        const resolved = result =>
          send({
            type: 'CTP_RESOLVE',
            promiseID,
            res: serialize(harden(result)),
          });
        const rejected = reason =>
          send({
            type: 'CTP_RESOLVE',
            promiseID,
            rej: serialize(harden(reason)),
          });
        E.when(
          val,
          resolved,
          rejected,
          // Propagate internal errors as rejections.
        ).catch(rejected);
      }
      // Now record the export in both valToSlot and slotToVal so we can look it
      // up from either the value or the slot name later.
      valToSlot.set(val, slot);
      importExportTables.markAsExported(slot, val);
    }

    // At this point, the value is guaranteed to be exported, so return the
    // associated slot number.
    const slot = valToSlot.get(val);
    assert.typeof(slot, 'string');
    sendSlot.add(slot);

    return slot;
  }
Called at marshalling time.  Either retrieves an existing export, or if
not yet exported, records this exported object.  If a promise, sets up a
promise listener to inform the other side when the promise is
fulfilled/broken.

@type {import('@endo/marshal').ConvertValToSlot<CapTPSlot>}