Optional methods in a binding

I have this binding:

type nonrec record('eventWithTime) = {
  emit: 'eventWithTime => unit,
  [@optional]
  replay: option(unit => array('eventWithTime)),
};
[@module "rrweb"] [@new]
external record: unit => record('eventWithTime) = "record";

To use it I am doing:

rrweb.record({
  emit: event => {
    Js.log2("record event: ", event);
  },
  replay: None
});

Im pretty sure the @optional decorator is not being used correctly. The idea is not have to pass replay if im not using it. How would I do that?

Thanks.

This is in ReScript formatting, but here is a sample (untested) binding for rrweb.record. Playground Link.

// Arbitrary JS Object Type.
// This can be manually defined if desired
type recordProp<'eventWithTime>

// The result of calling `rrweb.record`
// "The record method returns a function which can be called to stop events from firing"
// https://github.com/rrweb-io/rrweb/blob/master/guide.md#getting-started
type stopFn = unit => unit

// Format taken from your provided example
type callback<'eventWithTime> = 'eventWithTime => unit

// https://github.com/rrweb-io/rrweb/blob/master/guide.md#options
// Creates a function `recordProp` that creates objects of type `recordProp`'
// This could be named differently
@obj
external recordProp: (
  ~emit: callback<'eventWithTime>,
  ~checkoutEveryNth: 'a=?,
  ~checkoutEveryNms: 'b=?,
  ~blockClass: 'c=?,
  ~blockSelector: 'd=?,
  ~ignoreClass: 'e=?,
  ~maskTextClass: 'f=?,
  ~maskTextSelector: 'g=?,
  ~maskAllInputs: 'h=?,
  ~maskInputOptions: 'i=?,
  ~maskInputFn: 'j=?,
  ~maskTextFn: 'k=?,
  ~slimDOMOptions: 'l=?,
  ~inlineStylesheet: 'm=?,
  ~hooks: 'n=?,
  ~packFn: 'o=?,
  ~sampling: 'p=?,
  ~recordCanvas: 'q=?,
  ~inlineImages: 'r=?,
  ~collectFonts: 's=?,
  ~userTriggeredOnInput: 't=?,
  ~plugins: 'u=?,
  unit,
) => recordProp<'eventWithTime> = ""

// https://github.com/rrweb-io/rrweb/blob/master/guide.md#getting-started
@module("rrweb")
external record: recordProp<'eventWithTime> => stopFn = "record"

// Example of creating a record prop with only `emit` defined
// Trailing () arguments are requred for `recordProp` to tell the compiler that is
// is done recieving optional arguments
let stopFn1 = record(recordProp(~emit=a => Js.log(a), ()))

// Example of creating a record prop with `emit` and something else defined
let stopFn2 = record(recordProp(~emit=a => Js.log(a), ~ignoreClass=true, ()))

stopFn1()
stopFn2()

I stubbed out a lot of the possible arguments, but these can be dropped entirely if you don’t want to support them. It utilizes this section of the documentation for creating JS objects from an arbitrary external binding with the @obj decorator.

I didn’t see replay listed in the options for record so I’m not sure how to help you there, but hopefully this helps!

1 Like

Thanks for taking the time to teach this. I had written out all the types which I see is not entirely necessary.

Im trying to duplicate what you have for Replayer

type play<'events> = {play: 'events => unit}
type replayer<'events> = {
  events: 'events,
  play: play<'events>,
}
@obj
external replayerOptions: (~events: 'eventWithTime, unit) => replayer<'events> =
  ""
// @new because its a class
@module("rrweb") @new
external replayer: replayer<'events> => play<'events> = "Replayer"

I use it like so:

let startReplay = e => {
    e->ReactEvent.Mouse.preventDefault;
    if (events->Js.Array.length < 10) {
      Js.log2(
        events->Js.Array.length,
        "it needs more than 10 events to play",
      );
    } else {
        
      let replayer = replayer(replayerOptions(~events, ()));

      replayer.play();
    };
  };

which gets me this error:

Any idea how to handle that/what that means?

Thanks again for teaching.

Looking at the source code for rrweb is seems to take the following

  constructor(
    events: Array<eventWithTime | string>,
    config?: Partial<playerConfig>,
  ) {

With the configs being entirely optional.

The main problem I see is that it expects the first argument to be either an array of events or a string. Currently you are providing it with a record / JS object with the shape of:

{
  events: 'eventWithTime
}

Another thing of note is that passing around the type parameter <'eventWithTime> is likely not required here unless that type actually changes from the library side. If is is always the same format you can remove the type parameters and just add an opaque type type eventWithTime at the top.

Here is a playground link that shows how I would personally write these bindings, though I’m not sure if this is the standard convention. type t is just a stand in type associated with the module they are in. You can give them more descriptive names if you desire. A new introduction to this example is the use of @send to call methods on class-like objects from JS.

1 Like

This was extremely helpful, sir. Thanks for taking the time to jump into this forum for the first time to help me.

The code almost runs. You get an error saying that you cant set property delay on event because its not an object. So made the Eventtime return an object so delay could be set and it works.

/* Define this in Rrweb.res rather than in file */
module Rrweb = {
  /* Contains things related to the event type */
  module EventWithTime = {
    type t<'a> = {event: 'a}

    // Equivalent to %identity type conversion, unsafe, only for demonstration
    let make = (any: 'a): t<'a> => {event: any}
  }

  module Record = {
    type options

    type callback<'a> = EventWithTime.t<'a> => unit

    // The result of calling `rrweb.record`
    // "The record method returns a function which can be called to stop events from firing"
    // https://github.com/rrweb-io/rrweb/blob/master/guide.md#getting-started
    type stopFn = unit => unit

    @obj
    external mkOptions: (
      ~emit: callback<'a>,
      ~checkoutEveryNth: 'a=?,
      ~checkoutEveryNms: 'b=?,
      ~blockClass: 'c=?,
      ~blockSelector: 'd=?,
      ~ignoreClass: 'e=?,
      ~maskTextClass: 'f=?,
      ~maskTextSelector: 'g=?,
      ~maskAllInputs: 'h=?,
      ~maskInputOptions: 'i=?,
      ~maskInputFn: 'j=?,
      ~maskTextFn: 'k=?,
      ~slimDOMOptions: 'l=?,
      ~inlineStylesheet: 'm=?,
      ~hooks: 'n=?,
      ~packFn: 'o=?,
      ~sampling: 'p=?,
      ~recordCanvas: 'q=?,
      ~inlineImages: 'r=?,
      ~collectFonts: 's=?,
      ~userTriggeredOnInput: 't=?,
      ~plugins: 'u=?,
      unit,
    ) => options = ""

    // https://github.com/rrweb-io/rrweb/blob/master/guide.md#getting-started
    @module("rrweb")
    external record: options => stopFn = "record"
  }

  /* Contains things related only to the Replayer */
  module Replayer = {
    // Stand in type for the Replayer class objects
    type t

    // Stand in type for the optional options record for the Replayer constructor
    type options

    @obj
    external mkOptions: (
    //   ~events: array<EventWithTime.t<'a>>,
      ~speed: float=?,
      // And all the other options [here](https://github.com/rrweb-io/rrweb/blob/master/guide.md#options-1)
      // Didn't want to type them this time
      unit,
    ) => options = ""

    /* Create the Replayer object */
    @module("rrweb") @new
    external make: array<EventWithTime.t<'a>> => t = "Replayer"

    /* Create the Replayer object */
    @module("rrweb") @new
    external makeWithOptions: (array<EventWithTime.t<'a>>, options) => t = "Replayer"

    /* Use @send to equivalently do Replayer.someFunc */
    @send
    external play: t => unit = "play"
  }
}

// Usage

// Optionally open the bindings to avoid typeing Rrweb a lot
open Rrweb

// Example of creating a record prop with only `emit` defined
// Trailing () arguments are requred for `recordProp` to tell the compiler that is
// is done recieving optional arguments
let stopFn = Record.record(Record.mkOptions(~emit=a => Js.log(a), ()))

stopFn()

/* Fake events */
let events: array<EventWithTime.t<'a>> = [EventWithTime.make("some"), EventWithTime.make("event")]
Js.log2("events", events)
/* Create the replayer object with the given events */
let replayer = Replayer.make(events)

/* Create the replayer object with the given events *and* options */
let replayerWithOptions = Replayer.makeWithOptions(events, Replayer.mkOptions(~speed=2.0, ()))

/* Compiles to replayer.play() */
replayer->Replayer.play
@react.component
let default = () => {
  <div> <h1> {React.string("rrweb")} </h1> </div>
}