Is there a way to create a typesafe event observer in rescript?

I have a type safe version in ts

namespace observer {
  export type EventMap<EventArgsMap> = {
    [Event in keyof EventArgsMap]: ((args: EventArgsMap[Event]) => void)[]
  }

  export type Destroyer = {
    destroy: () => void
  }

  export interface Observer<EventArgsMap> {
    bind<Event extends keyof EventArgsMap>(event: Event, cb: (args: EventArgsMap[Event]) => void): Destroyer
    fire<Event extends keyof EventArgsMap>(event: Event, args: EventArgsMap[Event]): void
  }

  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
  export const createObserver = <EventArgsMap>(): Observer<EventArgsMap> => {
    const eventMap: Partial<EventMap<EventArgsMap>> = {}

    const bind = <Event extends keyof EventArgsMap>(event: Event, cb: (args: EventArgsMap[Event]) => void): { destroy: () => void } => {
      const cbs = eventMap[event] ?? []
      const newCbs = [...cbs, cb]
      eventMap[event] = newCbs
      return {
        destroy: (): void => {
          const cbs = eventMap[event] ?? []
          const newCbs = cbs.filter(item => item !== cb)
          eventMap[event] = newCbs
        }
      }
    }

    const fire = (event: keyof EventArgsMap, args: EventArgsMap[typeof event]): void => {
      const cbs = eventMap[event] ?? []
      cbs.forEach(cb => cb(args))
    }

    return {
      bind,
      fire
    }
  }
}

const observerIns = observer.createObserver<{
  sayOffset: number
  startRender: void
}>()

observerIns.bind('sayOffset', (v) => {
  console.log(v + 1)
})
observerIns.bind('startRender', () => {
  
})

The ts version is type safe, as long as you transfer a EventArgMap type in createObserver, the bind and fire method will check the input types. But i am stucked in using rescript to create a type safe event observer

The playground is here:

1 Like

It’s not really an observer, but works similar to what you have:

// Act.res

type t<'a> = {
  mutable value: 'a,
  mutable maybeSubs: option<Set.t<'a => unit>>,
}

let make = initialValue => {
  value: initialValue,
  maybeSubs: None,
}

let get = act => act.value
let set = (act, value) => {
  act.value = value
  switch act.maybeSubs {
  | Some(subs) => subs->Set.forEach(sub => sub(value))
  | None => ()
  }
}
let subscribe = (act, fn) => {
  let set = switch act.maybeSubs {
  | Some(subs) => subs
  | None => {
      let newSet = Set.make()
      act.maybeSubs = Some(newSet)
      newSet
    }
  }
  set->Set.unsafeAdd(fn)->ignore
  () => set->Set.unsafeDelete(fn)->ignore
}

let use = act =>
  ReactExtra.useSyncExternalStore(
    ~subscribe=fn => act->subscribe(fn),
    ~getSnapshot=() => act->get,
    ~getServerSnapshot=() => act->get,
  )
// Act.resi

type t<'a>

let make: 'a => t<'a>

let get: t<'a> => 'a
let set: (t<'a>, 'a) => unit

let subscribe: (t<'a>, 'a => unit) => unit => unit

let use: t<'a> => 'a
2 Likes

@DZakh Thanks for the reply, i write some code in rescript, it works like the ts version at some point.

module type Action = {
  type t
}

module type Observer = {
  type action
  type t

  let make: unit => t

  let fire: (t, action) => unit
  let bind: (t, action => unit, unit) => unit
}

module MakeObserver = (Act: Action): (Observer with type action = Act.t) => {
  type action = Act.t
  type t = {mutable cbs: array<Act.t => unit>}
  let make = (): t => {
    {
      cbs: [],
    }
  }

  let fire = (observer: t, arg: Act.t): unit => {
    observer.cbs->Js.Array2.forEach(item => item(arg))
  }

  let bind = (observer: t, cb: Act.t => unit): (unit => unit) => {
    observer.cbs = observer.cbs->Js.Array2.concat([cb])
    () => {
      observer.cbs = observer.cbs->Js.Array2.filter(item => item !== cb)
    }
  }
}

module OffsetObserver = MakeObserver({
  type t = int
})

module RenderObserver = MakeObserver({
  type t = unit
})

type barObserver = {
  offset: OffsetObserver.t,
  render: RenderObserver.t,
}

let barObserver = {
  offset: OffsetObserver.make(),
  render: RenderObserver.make(),
}

let v = barObserver.offset->OffsetObserver.bind(v => {
  //
  assert false
})

let v1 = barObserver.render->RenderObserver.bind(v => {
  //
  assert false
})

1 Like

Good! Although, I don’t really think that it should be a functor. It’s my personal opinion. I don’t like them and almost never use

Yes, it’s not need using functor!

type observer<'act> = {
  bind: ('act => unit) => (unit => unit),
  fire: 'act => unit
}

let makeObserver = (): observer<'act> => {
  let cbs = ref([])
  let bind = (cb: 'act => unit): (unit => unit) => {
    cbs := cbs.contents->Js.Array2.concat([cb])
  	() => {
      cbs := cbs.contents->Js.Array2.filter(cb_ => cb_ !== cb)
    }
  }
  let fire = (act: 'act): unit => {
    cbs.contents->Js.Array2.forEach(cb => cb(act))
  }
  {
    bind,
    fire
  }
}

type barObserver = {
  offset: observer<int>,
  render: observer<unit>
}

let barObserver = {
  offset: makeObserver(),
  render: makeObserver()
}

let removeOffset = barObserver.offset.bind(v => {
  let _v = v + 1
})

let removeRender = barObserver.render.bind(() => {
  ()
})
4 Likes