aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/async/_event.lua
blob: 676ef0f8d092508dd6b89ed473878a3364ddcf7c (plain) (blame)
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
local async = require('vim.async._core')
local runtime = require('vim.async._runtime')

--- An event can be used to notify multiple tasks that some event has
--- happened. An Event object manages an internal flag that can be set to true
--- with the `set()` method and reset to `false` with the `clear()` method.
--- The `wait()` method blocks until the flag is set to `true`. The flag is
--- set to `false` initially.
--- @class vim.async.Event
--- @field private _is_set boolean
--- @field private _waiters (function|false)[]
local Event = {}
Event.__index = Event

--- @param waiters (function|false)[]
--- @return boolean
local function has_waiters(waiters)
  for _, waiter in ipairs(waiters) do
    if waiter then
      return true
    end
  end
  return false
end

--- Set the event.
---
--- All tasks waiting for event to be set will be awakened on a later event-loop
--- turn.
---
--- If `max_woken` is provided, only up to `max_woken` waiters will be woken.
--- If waiters are woken this way, the event is reset because the signal is
--- reserved for those waiters.
--- @param max_woken? integer
function Event:set(max_woken)
  if self._is_set then
    return
  end

  local limited = max_woken ~= nil
  if not has_waiters(self._waiters) then
    self._is_set = true
    return
  end

  self._is_set = true
  if limited then
    -- The signal is reserved for existing waiters and will be assigned on the
    -- scheduled turn. New waiters must not consume it first.
    self._is_set = false
  end

  runtime.schedule(function()
    local waiters = self._waiters
    local waiters_to_notify = {} --- @type function[]
    local limit = max_woken or math.huge
    while #waiters > 0 and #waiters_to_notify < limit do
      local waiter = table.remove(waiters, 1)
      if waiter then
        waiters_to_notify[#waiters_to_notify + 1] = waiter
      end
    end

    if limited and #waiters_to_notify == 0 and not has_waiters(waiters) then
      self._is_set = true
    end

    for _, waiter in ipairs(waiters_to_notify) do
      waiter()
    end
  end)
end

--- Wait until the event is set.
---
--- If the event is set, return immediately. Otherwise block until another
--- task calls set().
--- @async
function Event:wait()
  async.await(function(callback)
    if self._is_set then
      callback()
    else
      table.insert(self._waiters, callback)
      return {
        close = function(_, on_close)
          -- set() compacts the waiter list, so cancellation cannot rely on the
          -- original insertion index still pointing at this callback.
          for i, waiter in ipairs(self._waiters) do
            if waiter == callback then
              self._waiters[i] = false
              break
            end
          end
          if on_close then
            on_close()
          end
        end,
      }
    end
  end)
end

--- Clear (unset) the event.
---
--- Tasks awaiting on wait() will now block until the set() method is called
--- again.
function Event:clear()
  self._is_set = false
end

--- Create a new event.
---
--- An event can signal to multiple listeners to resume execution.
--- The event can be set from a non-async context.
--- @return vim.async.Event
return function()
  return setmetatable({
    _waiters = {},
    _is_set = false,
  }, Event)
end