summaryrefslogtreecommitdiff
path: root/indra/newview/scripts/lua
diff options
context:
space:
mode:
authorNat Goodspeed <nat@lindenlab.com>2024-03-06 09:56:45 -0500
committerNat Goodspeed <nat@lindenlab.com>2024-03-06 09:56:45 -0500
commitc621fc39fc4ac25482fbc1090b8067c4187de176 (patch)
treead04aa2f0658cbf1b2ee0cce1b7aede826e7d2ed /indra/newview/scripts/lua
parentb4bef56b20fbeaaea60b20bd84d1569ed76cf29b (diff)
WIP: Unfinished Queue.lua, WaitQueue.lua, ErrorQueue.lua, leap.lua.
Also qtest.lua to exercise the queue classes and inspect.lua (from https://github.com/kikito/inspect.lua) for debugging.
Diffstat (limited to 'indra/newview/scripts/lua')
-rw-r--r--indra/newview/scripts/lua/ErrorQueue.lua32
-rw-r--r--indra/newview/scripts/lua/Queue.lua41
-rw-r--r--indra/newview/scripts/lua/WaitQueue.lua68
-rw-r--r--indra/newview/scripts/lua/inspect.lua371
-rw-r--r--indra/newview/scripts/lua/leap.lua284
-rw-r--r--indra/newview/scripts/lua/qtest.lua47
6 files changed, 823 insertions, 20 deletions
diff --git a/indra/newview/scripts/lua/ErrorQueue.lua b/indra/newview/scripts/lua/ErrorQueue.lua
new file mode 100644
index 0000000000..e6279f8411
--- /dev/null
+++ b/indra/newview/scripts/lua/ErrorQueue.lua
@@ -0,0 +1,32 @@
+-- ErrorQueue isa WaitQueue with the added feature that a producer can push an
+-- error through the queue. When that error is dequeued, the consumer will
+-- throw that error.
+
+local WaitQueue = require('WaitQueue')
+
+ErrorQueue = WaitQueue:new()
+
+function ErrorQueue:Enqueue(value)
+ -- normal value, not error
+ WaitQueue:Enqueue({ false, value })
+end
+
+function ErrorQueue:Error(message)
+ -- flag this entry as an error message
+ WaitQueue:Enqueue({ true, message })
+end
+
+function ErrorQueue:Dequeue()
+ local errflag, value = table.unpack(WaitQueue:Dequeue())
+ if errflag == nil then
+ -- queue has been closed, tell caller
+ return nil
+ end
+ if errflag then
+ -- 'value' is a message pushed by Error()
+ error(value)
+ end
+ return value
+end
+
+return ErrorQueue
diff --git a/indra/newview/scripts/lua/Queue.lua b/indra/newview/scripts/lua/Queue.lua
index e178ad9969..b0a5a87f87 100644
--- a/indra/newview/scripts/lua/Queue.lua
+++ b/indra/newview/scripts/lua/Queue.lua
@@ -1,40 +1,41 @@
--- from https://create.roblox.com/docs/luau/queues#implementing-queues
+-- from https://create.roblox.com/docs/luau/queues#implementing-queues,
+-- amended per https://www.lua.org/pil/16.1.html
local Queue = {}
-Queue.__index = Queue
-function Queue.new()
- local self = setmetatable({}, Queue)
+function Queue:new()
+ local obj = setmetatable({}, self)
+ self.__index = self
- self._first = 0
- self._last = -1
- self._queue = {}
+ obj._first = 0
+ obj._last = -1
+ obj._queue = {}
- return self
+ return obj
end
-- Check if the queue is empty
function Queue:IsEmpty()
- return self._first > self._last
+ return self._first > self._last
end
-- Add a value to the queue
function Queue:Enqueue(value)
- local last = self._last + 1
- self._last = last
- self._queue[last] = value
+ local last = self._last + 1
+ self._last = last
+ self._queue[last] = value
end
-- Remove a value from the queue
function Queue:Dequeue()
- local first = self._first
- if self:IsEmpty() then
- return nil
- end
- local value = self._queue[first]
- self._queue[first] = nil
- self._first = first + 1
- return value
+ if self:IsEmpty() then
+ return nil
+ end
+ local first = self._first
+ local value = self._queue[first]
+ self._queue[first] = nil
+ self._first = first + 1
+ return value
end
return Queue
diff --git a/indra/newview/scripts/lua/WaitQueue.lua b/indra/newview/scripts/lua/WaitQueue.lua
new file mode 100644
index 0000000000..05d2056085
--- /dev/null
+++ b/indra/newview/scripts/lua/WaitQueue.lua
@@ -0,0 +1,68 @@
+-- WaitQueue isa Queue with the added feature that when the queue is empty,
+-- the Dequeue() operation blocks the calling coroutine until some other
+-- coroutine Enqueue()s a new value.
+
+local Queue = require('Queue')
+
+WaitQueue = Queue:new()
+
+function WaitQueue:new()
+ local obj = setmetatable(Queue:new(), self)
+ self.__index = self
+ obj._waiters = {}
+ obj._closed = false
+ return obj
+end
+
+function WaitQueue:Enqueue(value)
+ if self._closed then
+ error("can't Enqueue() on closed Queue")
+ end
+ Queue:Enqueue(value)
+ -- WaitQueue is designed to support multi-producer, multi-consumer use
+ -- cases. With multiple consumers, if more than one is trying to
+ -- Dequeue() from an empty WaitQueue, we'll have multiple waiters.
+ -- Unlike OS threads, with cooperative concurrency it doesn't make sense
+ -- to "notify all": we need resume only one of the waiting Dequeue()
+ -- callers. But since resuming that caller might entail either Enqueue()
+ -- or Dequeue() calls, recheck every time around to see if we must resume
+ -- another waiting coroutine.
+ while not self:IsEmpty() and #self._waiters do
+ -- pop the oldest waiting coroutine instead of the most recent, for
+ -- more-or-less round robin fairness
+ local waiter = table.remove(self._waiters, 1)
+ -- don't pass the head item: let the resumed coroutine retrieve it
+ local ok, message = coroutine.resume(waiter)
+ -- if resuming that waiter encountered an error, don't swallow it
+ if not ok then
+ error(message)
+ end
+ end
+end
+
+function WaitQueue:Dequeue()
+ while self:IsEmpty() do
+ -- Don't check for closed until the queue is empty: producer can close
+ -- the queue while there are still items left, and we want the
+ -- consumer(s) to retrieve those last few items.
+ if self._closed then
+ return nil
+ end
+ local coro = coroutine.running()
+ if coro == nil then
+ error("WaitQueue:Dequeue() trying to suspend main coroutine")
+ end
+ -- add the running coroutine to the list of waiters
+ table.insert(self._waiters, coro)
+ -- then let somebody else run
+ coroutine.yield()
+ end
+ -- here we're sure this queue isn't empty
+ return Queue:Dequeue()
+end
+
+function WaitQueue:close()
+ self._closed = true
+end
+
+return WaitQueue
diff --git a/indra/newview/scripts/lua/inspect.lua b/indra/newview/scripts/lua/inspect.lua
new file mode 100644
index 0000000000..9900a0b81b
--- /dev/null
+++ b/indra/newview/scripts/lua/inspect.lua
@@ -0,0 +1,371 @@
+local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table
+local inspect = {Options = {}, }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+inspect._VERSION = 'inspect.lua 3.1.0'
+inspect._URL = 'http://github.com/kikito/inspect.lua'
+inspect._DESCRIPTION = 'human-readable representations of tables'
+inspect._LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2022 Enrique GarcĂ­a Cota
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+]]
+inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end })
+inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end })
+
+local tostring = tostring
+local rep = string.rep
+local match = string.match
+local char = string.char
+local gsub = string.gsub
+local fmt = string.format
+
+local _rawget
+if rawget then
+ _rawget = rawget
+else
+ _rawget = function(t, k) return t[k] end
+end
+
+local function rawpairs(t)
+ return next, t, nil
+end
+
+
+
+local function smartQuote(str)
+ if match(str, '"') and not match(str, "'") then
+ return "'" .. str .. "'"
+ end
+ return '"' .. gsub(str, '"', '\\"') .. '"'
+end
+
+
+local shortControlCharEscapes = {
+ ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
+ ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127",
+}
+local longControlCharEscapes = { ["\127"] = "\127" }
+for i = 0, 31 do
+ local ch = char(i)
+ if not shortControlCharEscapes[ch] then
+ shortControlCharEscapes[ch] = "\\" .. i
+ longControlCharEscapes[ch] = fmt("\\%03d", i)
+ end
+end
+
+local function escape(str)
+ return (gsub(gsub(gsub(str, "\\", "\\\\"),
+ "(%c)%f[0-9]", longControlCharEscapes),
+ "%c", shortControlCharEscapes))
+end
+
+local luaKeywords = {
+ ['and'] = true,
+ ['break'] = true,
+ ['do'] = true,
+ ['else'] = true,
+ ['elseif'] = true,
+ ['end'] = true,
+ ['false'] = true,
+ ['for'] = true,
+ ['function'] = true,
+ ['goto'] = true,
+ ['if'] = true,
+ ['in'] = true,
+ ['local'] = true,
+ ['nil'] = true,
+ ['not'] = true,
+ ['or'] = true,
+ ['repeat'] = true,
+ ['return'] = true,
+ ['then'] = true,
+ ['true'] = true,
+ ['until'] = true,
+ ['while'] = true,
+}
+
+local function isIdentifier(str)
+ return type(str) == "string" and
+ not not str:match("^[_%a][_%a%d]*$") and
+ not luaKeywords[str]
+end
+
+local flr = math.floor
+local function isSequenceKey(k, sequenceLength)
+ return type(k) == "number" and
+ flr(k) == k and
+ 1 <= (k) and
+ k <= sequenceLength
+end
+
+local defaultTypeOrders = {
+ ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
+ ['function'] = 5, ['userdata'] = 6, ['thread'] = 7,
+}
+
+local function sortKeys(a, b)
+ local ta, tb = type(a), type(b)
+
+
+ if ta == tb and (ta == 'string' or ta == 'number') then
+ return (a) < (b)
+ end
+
+ local dta = defaultTypeOrders[ta] or 100
+ local dtb = defaultTypeOrders[tb] or 100
+
+
+ return dta == dtb and ta < tb or dta < dtb
+end
+
+local function getKeys(t)
+
+ local seqLen = 1
+ while _rawget(t, seqLen) ~= nil do
+ seqLen = seqLen + 1
+ end
+ seqLen = seqLen - 1
+
+ local keys, keysLen = {}, 0
+ for k in rawpairs(t) do
+ if not isSequenceKey(k, seqLen) then
+ keysLen = keysLen + 1
+ keys[keysLen] = k
+ end
+ end
+ table.sort(keys, sortKeys)
+ return keys, keysLen, seqLen
+end
+
+local function countCycles(x, cycles)
+ if type(x) == "table" then
+ if cycles[x] then
+ cycles[x] = cycles[x] + 1
+ else
+ cycles[x] = 1
+ for k, v in rawpairs(x) do
+ countCycles(k, cycles)
+ countCycles(v, cycles)
+ end
+ countCycles(getmetatable(x), cycles)
+ end
+ end
+end
+
+local function makePath(path, a, b)
+ local newPath = {}
+ local len = #path
+ for i = 1, len do newPath[i] = path[i] end
+
+ newPath[len + 1] = a
+ newPath[len + 2] = b
+
+ return newPath
+end
+
+
+local function processRecursive(process,
+ item,
+ path,
+ visited)
+ if item == nil then return nil end
+ if visited[item] then return visited[item] end
+
+ local processed = process(item, path)
+ if type(processed) == "table" then
+ local processedCopy = {}
+ visited[item] = processedCopy
+ local processedKey
+
+ for k, v in rawpairs(processed) do
+ processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
+ if processedKey ~= nil then
+ processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
+ end
+ end
+
+ local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
+ if type(mt) ~= 'table' then mt = nil end
+ setmetatable(processedCopy, mt)
+ processed = processedCopy
+ end
+ return processed
+end
+
+local function puts(buf, str)
+ buf.n = buf.n + 1
+ buf[buf.n] = str
+end
+
+
+
+local Inspector = {}
+
+
+
+
+
+
+
+
+
+
+local Inspector_mt = { __index = Inspector }
+
+local function tabify(inspector)
+ puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
+end
+
+function Inspector:getId(v)
+ local id = self.ids[v]
+ local ids = self.ids
+ if not id then
+ local tv = type(v)
+ id = (ids[tv] or 0) + 1
+ ids[v], ids[tv] = id, id
+ end
+ return tostring(id)
+end
+
+function Inspector:putValue(v)
+ local buf = self.buf
+ local tv = type(v)
+ if tv == 'string' then
+ puts(buf, smartQuote(escape(v)))
+ elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
+ tv == 'cdata' or tv == 'ctype' then
+ puts(buf, tostring(v))
+ elseif tv == 'table' and not self.ids[v] then
+ local t = v
+
+ if t == inspect.KEY or t == inspect.METATABLE then
+ puts(buf, tostring(t))
+ elseif self.level >= self.depth then
+ puts(buf, '{...}')
+ else
+ if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end
+
+ local keys, keysLen, seqLen = getKeys(t)
+
+ puts(buf, '{')
+ self.level = self.level + 1
+
+ for i = 1, seqLen + keysLen do
+ if i > 1 then puts(buf, ',') end
+ if i <= seqLen then
+ puts(buf, ' ')
+ self:putValue(t[i])
+ else
+ local k = keys[i - seqLen]
+ tabify(self)
+ if isIdentifier(k) then
+ puts(buf, k)
+ else
+ puts(buf, "[")
+ self:putValue(k)
+ puts(buf, "]")
+ end
+ puts(buf, ' = ')
+ self:putValue(t[k])
+ end
+ end
+
+ local mt = getmetatable(t)
+ if type(mt) == 'table' then
+ if seqLen + keysLen > 0 then puts(buf, ',') end
+ tabify(self)
+ puts(buf, '<metatable> = ')
+ self:putValue(mt)
+ end
+
+ self.level = self.level - 1
+
+ if keysLen > 0 or type(mt) == 'table' then
+ tabify(self)
+ elseif seqLen > 0 then
+ puts(buf, ' ')
+ end
+
+ puts(buf, '}')
+ end
+
+ else
+ puts(buf, fmt('<%s %d>', tv, self:getId(v)))
+ end
+end
+
+
+
+
+function inspect.inspect(root, options)
+ options = options or {}
+
+ local depth = options.depth or (math.huge)
+ local newline = options.newline or '\n'
+ local indent = options.indent or ' '
+ local process = options.process
+
+ if process then
+ root = processRecursive(process, root, {}, {})
+ end
+
+ local cycles = {}
+ countCycles(root, cycles)
+
+ local inspector = setmetatable({
+ buf = { n = 0 },
+ ids = {},
+ cycles = cycles,
+ depth = depth,
+ level = 0,
+ newline = newline,
+ indent = indent,
+ }, Inspector_mt)
+
+ inspector:putValue(root)
+
+ return table.concat(inspector.buf)
+end
+
+setmetatable(inspect, {
+ __call = function(_, root, options)
+ return inspect.inspect(root, options)
+ end,
+})
+
+return inspect
diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua
new file mode 100644
index 0000000000..351e1bf007
--- /dev/null
+++ b/indra/newview/scripts/lua/leap.lua
@@ -0,0 +1,284 @@
+-- Lua implementation of LEAP (LLSD Event API Plugin) protocol
+--
+-- This module supports Lua scripts run by the Second Life viewer.
+--
+-- LEAP protocol passes LLSD objects, converted to/from Lua tables, in both
+-- directions. A typical LLSD object is a map containing keys 'pump' and
+-- 'data'.
+--
+-- The viewer's Lua post_to(pump, data) function posts 'data' to the
+-- LLEventPump 'pump'. This is typically used to engage an LLEventAPI method.
+--
+-- Similarly, the viewer gives each Lua script its own LLEventPump with a
+-- unique name. That name is returned by get_event_pumps(). Every event
+-- received on that LLEventPump is queued for retrieval by get_event_next(),
+-- which returns (pump, data): the name of the LLEventPump on which the event
+-- was received and the received event data. When the queue is empty,
+-- get_event_next() blocks the calling Lua script until the next event is
+-- received.
+
+local leap = {}
+
+-- _reply: string name of reply LLEventPump. Any events the viewer posts to
+-- this pump will be queued for get_event_next(). We usually specify it as the
+-- reply pump for requests to internal viewer services.
+-- _command: string name of command LLEventPump. post_to(_command, ...)
+-- engages LLLeapListener operations such as listening on a specified other
+-- LLEventPump, etc.
+leap._reply, leap._command = get_event_pumps()
+-- Dict of features added to the LEAP protocol since baseline implementation.
+-- Before engaging a new feature that might break an older viewer, we can
+-- check for the presence of that feature key. This table is solely about the
+-- LEAP protocol itself, the way we communicate with the viewer. To discover
+-- whether a given listener exists, or supports a particular operation, use
+-- _command's "getAPI" operation.
+-- For Lua, _command's "getFeatures" operation suffices?
+-- leap._features = {}
+
+-- Each outstanding request() or generate() call has a corresponding
+-- WaitForReqid object (later in this module) to handle the
+-- response(s). If an incoming event contains an echoed ["reqid"] key,
+-- we can look up the appropriate WaitForReqid object more efficiently
+-- in a dict than by tossing such objects into the usual waitfors list.
+-- Note: the ["reqid"] must be unique, otherwise we could end up
+-- replacing an earlier WaitForReqid object in self.pending with a
+-- later one. That means that no incoming event will ever be given to
+-- the old WaitForReqid object. Any coroutine waiting on the discarded
+-- WaitForReqid object would therefore wait forever.
+leap._pending = {}
+-- Our consumer will instantiate some number of WaitFor subclass objects.
+-- As these are traversed in descending priority order, we must keep
+-- them in a list.
+leap._waitfors = {}
+-- It has been suggested that we should use UUIDs as ["reqid"] values,
+-- since UUIDs are guaranteed unique. However, as the "namespace" for
+-- ["reqid"] values is our very own _reply pump, we can get away with
+-- an integer.
+leap._reqid = 0
+
+-- get the name of the reply pump
+function leap.replypump()
+ return leap._reply
+end
+
+-- get the name of the command pump
+function leap.cmdpump()
+ return leap._command
+end
+
+-- Fire and forget. Send the specified request LLSD, expecting no reply.
+-- In fact, should the request produce an eventual reply, it will be
+-- treated as an unsolicited event.
+--
+-- See also request(), generate().
+function leap.send(pump, data, reqid)
+ local data = data
+ if type(data) == 'table' then
+ data = table.clone(data)
+ data['reply'] = leap._reply
+ if reqid ~= nil then
+ data['reqid'] = reqid
+ end
+ end
+ post_to(pump, data)
+end
+
+-- Send the specified request LLSD, expecting exactly one reply. Block
+-- the calling coroutine until we receive that reply.
+--
+-- Every request() (or generate()) LLSD block we send will get stamped
+-- with a distinct ["reqid"] value. The requested event API must echo the
+-- same ["reqid"] field in each reply associated with that request. This way
+-- we can correctly dispatch interleaved replies from different requests.
+--
+-- If the desired event API doesn't support the ["reqid"] echo convention,
+-- you should use send() instead -- since request() or generate() would
+-- wait forever for a reply stamped with that ["reqid"] -- and intercept
+-- any replies using WaitFor.
+--
+-- Unless the request data already contains a ["reply"] key, we insert
+-- reply=self.replypump to try to ensure that the expected reply will be
+-- returned over the socket.
+function leap.request(pump, data)
+ local reqid = leap._requestSetup(pump, data)
+ local ok, response = pcall(leap._pending[reqid].wait)
+ -- kill off temporary WaitForReqid object, even if error
+ leap._pending[reqid] = nil
+ if ok then
+ return response
+ else
+ error(response)
+ end
+end
+
+-- common setup code shared by request() and generate()
+function leap._requestSetup(pump, data)
+ -- invent a new, unique reqid
+ local reqid = leap._reqid
+ leap._reqid += 1
+ -- Instantiate a new WaitForReqid object. The priority is irrelevant
+ -- because, unlike the WaitFor base class, WaitForReqid does not
+ -- self-register on our leap._waitfors list. Instead, capture the new
+ -- WaitForReqid object in leap._pending so _dispatch() can find it.
+ leap._pending[reqid] = WaitForReqid.new(reqid)
+ -- Pass reqid to send() to stamp it into (a copy of) the request data.
+ leap.send(pump, data, reqid)
+ return reqid
+end
+
+-- Send the specified request LLSD, expecting an arbitrary number of replies.
+-- Each one is yielded on receipt. If you omit checklast, this is an infinite
+-- generator; it's up to the caller to recognize when the last reply has been
+-- received, and stop resuming for more.
+--
+-- If you pass checklast=<callable accepting(event)>, each response event is
+-- passed to that callable (after the yield). When the callable returns
+-- True, the generator terminates in the usual way.
+--
+-- See request() remarks about ["reqid"].
+function leap.generate(pump, data, checklast)
+ -- Invent a new, unique reqid. Arrange to handle incoming events
+ -- bearing that reqid. Stamp the outbound request with that reqid, and
+ -- send it.
+ local reqid = leap._requestSetup(pump, data)
+ local ok, response
+ repeat
+ ok, response = pcall(leap._pending[reqid].wait)
+ if not ok then
+ break
+ end
+ coroutine.yield(response)
+ until checklast and checklast(response)
+ -- If we break the above loop, whether or not due to error, clean up.
+ leap._pending[reqid] = nil
+ if not ok then
+ error(response)
+ end
+end
+
+-- Kick off response processing. The calling script must create and resume one
+-- or more coroutines to perform viewer requests using send(), request() or
+-- generate() before calling this function to handle responses.
+--
+-- While waiting for responses from the viewer, the C++ coroutine running the
+-- calling Lua script is blocked: no other Lua coroutine is running.
+function leap.process()
+ local ok, pump, data
+ while true do
+ ok, pump, data = pcall(get_event_next)
+ if not ok then
+ break
+ end
+ leap._dispatch(pump, data)
+ end
+ -- we're done: clean up all pending coroutines
+ for i, waitfor in pairs(leap._pending) do
+ waitfor._exception(pump)
+ end
+ for i, waitfor in pairs(leap._waitfors) do
+ waitfor._exception(pump)
+ end
+end
+
+-- Route incoming (pump, data) event to the appropriate waiting coroutine.
+function leap._dispatch(pump, data)
+ local reqid = data['reqid']
+ -- if the response has no 'reqid', it's not from request() or generate()
+ if reqid == nil then
+ return leap._unsolicited(pump, data)
+ end
+ -- have reqid; do we have a WaitForReqid?
+ local waitfor = leap._pending[reqid]
+ if waitfor == nil then
+ return leap._unsolicited(pump, data)
+ end
+ -- found the right WaitForReqid object, let it handle the event
+ data['reqid'] = nil
+ waitfor._handle(pump, data)
+end
+
+-- Handle an incoming (pump, data) event with no recognizable ['reqid']
+function leap._unsolicited(pump, data)
+ -- we maintain waitfors in descending priority order, so the first waitfor
+ -- to claim this event is the one with the highest priority
+ for i, waitfor in pairs(leap._waitfors) do
+ if waitfor._handle(pump, data) then
+ return
+ end
+ end
+ print_debug('_unsolicited(', pump, ', ', data, ') discarding unclaimed event')
+end
+
+-- called by WaitFor.enable()
+function leap._registerWaitFor(waitfor)
+ table.insert(leap._waitfors, waitfor)
+ -- keep waitfors sorted in descending order of specified priority
+ table.sort(leap._waitfors,
+ function (lhs, rhs) return lhs.priority > rhs.priority end)
+end
+
+-- called by WaitFor.disable()
+function leap._unregisterWaitFor(waitfor)
+ for i, w in pairs(leap._waitfors) do
+ if w == waitfor then
+ leap._waitfors[i] = nil
+ break
+ end
+ end
+end
+
+-- ******************************************************************************
+-- WaitFor and friends
+-- ******************************************************************************
+
+-- An unsolicited event is handled by the highest-priority WaitFor subclass
+-- object willing to accept it. If no such object is found, the unsolicited
+-- event is discarded.
+--
+-- - First, instantiate a WaitFor subclass object to register its interest in
+-- some incoming event(s). WaitFor instances are self-registering; merely
+-- instantiating the object suffices.
+-- - Any coroutine may call a given WaitFor object's wait() method. This blocks
+-- the calling coroutine until a suitable event arrives.
+-- - WaitFor's constructor accepts a float priority. Every incoming event
+-- (other than those claimed by request() or generate()) is passed to each
+-- extant WaitFor.filter() method in descending priority order. The first
+-- such filter() to return nontrivial data claims that event.
+-- - At that point, the blocked wait() call on that WaitFor object returns the
+-- item returned by filter().
+-- - WaitFor contains a queue. Multiple arriving events claimed by that WaitFor
+-- object's filter() method are added to the queue. Naturally, until the
+-- queue is empty, calling wait() immediately returns the front entry.
+--
+-- It's reasonable to instantiate a WaitFor subclass whose filter() method
+-- unconditionally returns the incoming event, and whose priority places it
+-- last in the list. This object will enqueue every unsolicited event left
+-- unclaimed by other WaitFor subclass objects.
+--
+-- It's not strictly necessary to associate a WaitFor object with exactly one
+-- coroutine. You might have multiple "worker" coroutines drawing from the same
+-- WaitFor object, useful if the work being done per event might itself involve
+-- "blocking" operations. Or a given coroutine might sample a number of WaitFor
+-- objects in round-robin fashion... etc. etc. Nonetheless, it's
+-- straightforward to designate one coroutine for each WaitFor object.
+leap.WaitFor = {}
+
+function leap.WaitFor:new()
+ obj = setmetatable({}, self)
+ self.__index = self
+
+
+
+ self._first = 0
+ self._last = -1
+ self._queue = {}
+
+ return obj
+end
+
+-- Check if the queue is empty
+function Queue:IsEmpty()
+ return self._first > self._last
+end
+
+return leap
diff --git a/indra/newview/scripts/lua/qtest.lua b/indra/newview/scripts/lua/qtest.lua
new file mode 100644
index 0000000000..16a54be0d1
--- /dev/null
+++ b/indra/newview/scripts/lua/qtest.lua
@@ -0,0 +1,47 @@
+-- Exercise the Queue, WaitQueue, ErrorQueue family
+
+Queue = require('Queue')
+WaitQueue = require('WaitQueue')
+ErrorQueue = require('ErrorQueue')
+
+q1 = Queue:new()
+q2 = Queue:new()
+
+q1:Enqueue(17)
+
+assert(not q1:IsEmpty())
+assert(q2:IsEmpty())
+assert(q1:Dequeue() == 17)
+assert(q1:Dequeue() == nil)
+assert(q2:Dequeue() == nil)
+
+q1 = WaitQueue:new()
+
+inspect = require('inspect')
+print(inspect(q1))
+
+q2 = WaitQueue:new()
+result = {}
+
+values = { 1, 1, 2, 3, 5, 8, 13, 21 }
+for i, value in pairs(values) do
+ q1:Enqueue(value)
+end
+
+function consumer(desc, q)
+ print('consumer(', desc, ') start')
+ local value = q:Dequeue()
+ while value ~= nil do
+ table.insert(result, value)
+ value = q:Dequeue()
+ end
+ print('consumer(', desc, ') done')
+end
+
+coa = coroutine.create(consumer)
+cob = coroutine.create(consumer)
+coroutine.resume(coa, 'a', q1)
+coroutine.resume(cob, 'b', q1)
+
+assert(result == values)
+