export type TestExpect = {
	Not: (self: TestExpect) -> TestExpect,
	ToBe: (self: TestExpect, value: any) -> (),
	ToBeNaN: (self: TestExpect, value: any) -> (),
	ToBeNil: (self: TestExpect) -> (),
	ToBeOk: (self: TestExpect) -> (),
	ToBeTruthy: (self: TestExpect) -> (),
	ToBeFalsy: (self: TestExpect) -> (),
	ToThrow: (self: TestExpect, err: any?) -> (),
	ToHaveLength: (self: TestExpect, length: number) -> (),
	ToHaveProperty: (self: TestExpect, property: string) -> (),
	ToBeGreaterThan: (self: TestExpect, value: number) -> (),
	ToBeGreaterThanOrEqual: (self: TestExpect, value: number) -> (),
	ToBeLessThan: (self: TestExpect, value: number) -> (),
	ToBeLessThanOrEqual: (self: TestExpect, value: number) -> (),
	ToContain: (self: TestExpect, value: any) -> (),
	ToBeNear: (self: TestExpect, value: number, epsilon: number) -> (),
	ToBeA: (self: TestExpect, typeOf: string) -> (),
}

local TestExpect = {}
TestExpect.__index = TestExpect

function TestExpect:ToBe(value: any)
	local condition = self.Value == value
	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "!=" else "=="} {value}`, 0)
	end
end

function TestExpect:ToBeNil()
	local condition = self.Value == nil
	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "!=" else "=="} nil`, 0)
	end
end

function TestExpect:ToBeOk()
	local condition = self.Value ~= nil
	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "is not" else "is"} ok`, 0)
	end
end

function TestExpect:ToBeNaN()
	local condition = self.Value ~= self.Value
	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "!=" else "=="} NaN`, 0)
	end
end

function TestExpect:ToThrow(err: any?)
	local condition: boolean
	if err ~= nil then
		condition = (not self.Success) and self.Err == err
	else
		condition = not self.Success
	end

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{if self.Flip then "expected to not throw" else "expected to throw"}`, 0)
	end
end

function TestExpect:ToBeTruthy()
	local condition = not not self.Value

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "not truthy" else "truthy"}`, 0)
	end
end

function TestExpect:ToBeFalsy()
	local condition = not self.Value

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "not falsy" else "falsy"}`, 0)
	end
end

function TestExpect:ToHaveLength(length: number)
	local t = typeof(self.Value)
	local validType = t == "string" or t == "table"
	local condition = validType and #self.Value == length

	if self.Flip and validType then
		condition = not condition
	end

	if not condition then
		if validType then
			error(`{t} length ({#self.Value}) {if self.Flip then "!=" else "=="} {length}`, 0)
		else
			error(`invalid type "{t}" (expected table or string)`, 0)
		end
	end
end

function TestExpect:ToHaveProperty(property: string)
	local t = typeof(self.Value)
	local validType = t == "table"
	local condition = validType and self.Value[property] ~= nil

	if self.Flip and validType then
		condition = not condition
	end

	if not condition then
		if validType then
			error(`table {if self.Flip then "does not contain" else "contains"} property "{property}"`, 0)
		else
			error(`invalid type "{t}" (expected table)`, 0)
		end
	end
end

function TestExpect:ToBeGreaterThan(value: number)
	local condition = self.Value > value

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "<=" else ">"} {value}`, 0)
	end
end

function TestExpect:ToBeGreaterThanOrEqual(value: number)
	local condition = self.Value >= value

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "<" else ">="} {value}`, 0)
	end
end

function TestExpect:ToBeLessThan(value: number)
	local condition = self.Value < value

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then ">=" else "<"} {value}`, 0)
	end
end

function TestExpect:ToBeLessThanOrEqual(value: number)
	local condition = self.Value <= value

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then ">" else "<="} {value}`, 0)
	end
end

function TestExpect:ToContain(value: any)
	local t = typeof(self.Value)
	local valid = t == "table" or t == "string"
	if t == "string" then
		valid = typeof(value) == "string"
	end

	local condition = false

	if valid then
		if t == "table" then
			condition = table.find(self.Value, value) ~= nil
		else
			condition = string.find(self.Value, value) ~= nil
		end
	end

	if self.Flip and valid then
		condition = not condition
	end

	if not condition then
		if valid then
			error(`"{value}" {if self.Flip then "not in" else "in"} {t}`, 0)
		else
			error(`invalid type "{t}" (expected table or string)`, 0)
		end
	end
end

function TestExpect:ToBeA(t: string)
	local condition = typeof(self.Value) == t

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`"{typeof(self.Value)}" {if self.Flip then "!=" else "=="} "{t}"`, 0)
	end
end

function TestExpect:ToBeNear(value: number, epsilon: number)
	local condition = math.abs(self.Value - value) < epsilon

	if self.Flip then
		condition = not condition
	end

	if not condition then
		error(`{self.Value} {if self.Flip then "is not near" else "is near"} {value}`, 0)
	end
end

function TestExpect:Not()
	self.Flip = not self.Flip
	return self
end

------------------------------------------------------------------------------------------------------------

export type TestContext = {
	Test: (self: TestContext, name: string, fn: () -> ()) -> (),
	Describe: (self: TestContext, name: string, fn: () -> ()) -> (),
	Expect: (self: TestContext, value: any) -> TestExpect,
	BeforeAll: (self: TestContext, fn: () -> ()) -> (),
	AfterAll: (self: TestContext, fn: () -> ()) -> (),
	BeforeEach: (self: TestContext, fn: () -> ()) -> (),
	AfterEach: (self: TestContext, fn: () -> ()) -> (),
}

type TestContextGroup = {
	Name: string,
	Items: { any },
	AnyFail: boolean,
	AfterAllFns: { () -> () },
	BeforeEachFns: { () -> () },
	AfterEachFns: { () -> () },
}

local function CreateGroup(name: string): TestContextGroup
	return {
		Name = name,
		Items = {},
		AnyFail = false,
		AfterAllFns = {},
		BeforeEachFns = {},
		AfterEachFns = {},
	}
end

local TestContext = {}
TestContext.__index = TestContext

function TestContext.new(root: string): TestContext
	local testContext = setmetatable({
		TotalTests = 0,
		TotalFails = 0,
		Group = CreateGroup(root),
	}, TestContext) :: any

	testContext.Current = testContext.Group

	return testContext
end

function TestContext:Test(name: string, fn: () -> ())
	for _, fn in self.Current.BeforeEachFns do
		fn()
	end
	local success, err = pcall(fn)
	self.Current.Items[name] = {
		Success = success,
		Err = err,
	}
	self.TotalTests += 1
	if not success then
		self.TotalFails += 1
		self.Current.AnyFail = true
	end
	for _, fn in self.Current.AfterEachFns do
		fn()
	end
end

function TestContext:Describe(name: string, fn: () -> ())
	local parentGroup = self.Current
	local group = CreateGroup(name)
	self.Current = group
	parentGroup.Items[name] = group
	for _, fn in parentGroup.BeforeEachFns do
		fn()
	end
	fn()
	self.Current = parentGroup
	if group.AnyFail then
		parentGroup.AnyFail = true
	end
	for _, fn in group.AfterAllFns do
		fn()
	end
	for _, fn in parentGroup.AfterEachFns do
		fn()
	end
end

function TestContext:Expect(value: any): TestExpect
	local resolvedValue = value
	local success, err = true, nil
	if typeof(value) == "function" then
		success, err = pcall(value)
		resolvedValue = if success then err else nil
	end
	return setmetatable({ Value = resolvedValue, Success = success, Err = err, Flip = false }, TestExpect) :: any
end

function TestContext:BeforeAll(fn: () -> ())
	fn()
end

function TestContext:AfterAll(fn: () -> ())
	table.insert(self.Current.AfterAllFns, fn)
end

function TestContext:BeforeEach(fn: () -> ())
	table.insert(self.Current.BeforeEachFns, fn)
end

function TestContext:AfterEach(fn: () -> ())
	table.insert(self.Current.AfterEachFns, fn)
end

------------------------------------------------------------------------------------------------------------

local Test = {}

function Test.run(ancestors: { Instance })
	local tests: { [string]: () -> () } = {}

	for _, ancestor in ancestors do
		for _, child in ancestor:GetDescendants() do
			if child:IsA("ModuleScript") and string.match(child.Name, "%.test$") ~= nil then
				local name = (string.match(child.Name, "(.+)%.test$")) or child.Name
				if name == "init" then
					name = child.Parent.Name
				end
				local fn = require(child)
				tests[name] = fn
			end
		end
	end

	local results: { TestContext } = {}
	local allPass = true

	local totalTests = 0
	local totalFails = 0
	local totalSuccesses = 0

	for name, test in tests do
		local context = TestContext.new(name)
		local success, err = pcall(test, context)
		if not success then
			error(`Test runner failed: {err}`)
		end
		table.insert(results, context)
		if context.Group.AnyFail then
			allPass = false
		end
		context.Current = nil
		totalTests += context.TotalTests
		totalFails += context.TotalFails
	end
	totalSuccesses = totalTests - totalFails

	-- Build results:
	local output = { "Test Results" }
	local function out(str: string)
		table.insert(output, str)
	end

	out("---------")
	out(`{totalTests} Total`)
	out(`{totalSuccesses} Passed`)
	out(`{totalFails} Failed`)
	out("---------")

	local tab = "    "
	for _, res in results do
		local function Output(group, lvl)
			out(`{string.rep(tab, lvl)}[{if group.AnyFail then "x" else "✓"}] {group.Name}`)
			for name, item in group.Items do
				if item.Items then
					Output(item, lvl + 1)
				else
					out(
						`{string.rep(tab, lvl + 1)}[{if not item.Success then "x" else "✓"}] {name}{if not item.Success
							then `\n{string.rep(tab, lvl + 2)}failed assertion: {item.Err}`
							else ""}`
					)
				end
			end
		end
		Output(res.Group, 0)
	end

	local outputTxt = table.concat(output, "\n")

	return {
		AllPass = allPass,
		Output = outputTxt,
	}
end

return Test


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

-- local BufferReader = require(script.Parent.BufferReader)
-- local BufferWriter = require(script.Parent.BufferWriter)

return function(ctx: Test.TestContext)
	ctx:Describe("Some test", function()
		ctx:Test("1 + 1 == 2", function()
			ctx:Expect(1 + 1):ToBe(2)
		end)
		ctx:Test("1 + 1 != 3", function()
			ctx:Expect(1 + 1):Not():ToBe(3)
		end)
		ctx:Describe("Nested group", function()
			ctx:Test("true == true", function()
				ctx:Expect(true):ToBe(true)
				ctx:Expect(function()
					return 32
				end):ToBe(32)
			end)
		end)
	end)
end


--!native

local BufferWriter = require(script.Parent.BufferWriter)
local DataTypeBuffer = require(script.Parent.DataTypeBuffer)
local Types = require(script.Parent.Types)

--[=[
	@class BufferReader

	A BufferReader is an abstraction wrapper for `buffer` objects
	that provides a convenient way of reading out data from buffers.
]=]
local BufferReader = {}
BufferReader.__index = BufferReader

function BufferReader.new(buf: string | buffer | Types.BufferWriter): Types.BufferReader
	if typeof(buf) == "string" then
		return BufferReader.fromString(buf)
	elseif typeof(buf) == "buffer" then
		return BufferReader.fromBuffer(buf)
	elseif typeof(buf) == "table" and getmetatable(buf :: any) == BufferWriter then
		return BufferReader.fromBuffer(buf:GetBuffer())
	end

	error(`expected string or buffer; got {typeof(buf)}`)
end

function BufferReader.fromBuffer(buf: buffer)
	local self = setmetatable({
		_buffer = buf,
		_size = buffer.len(buf),
		_cursor = 0,
	}, BufferReader)

	return self
end

function BufferReader.fromString(str: string)
	return BufferReader.fromBuffer(buffer.fromstring(str))
end

function BufferReader:_assertSize(desiredSize: number)
	if desiredSize > self._size then
		error(`cursor out of bounds`, 3)
	end
end

--[=[
	Read a signed 8-bit integer from the buffer.
]=]
function BufferReader:ReadInt8(): number
	self:_assertSize(self._cursor + 1)
	local n = buffer.readi8(self._buffer, self._cursor)
	self._cursor += 1
	return n
end

--[=[
	Read an unsigned 8-bit integer from the buffer.
]=]
function BufferReader:ReadUInt8(): number
	self:_assertSize(self._cursor + 1)
	local n = buffer.readu8(self._buffer, self._cursor)
	self._cursor += 1
	return n
end

--[=[
	Read a signed 16-bit integer from the buffer.
]=]
function BufferReader:ReadInt16(): number
	self:_assertSize(self._cursor + 2)
	local n = buffer.readi16(self._buffer, self._cursor)
	self._cursor += 2
	return n
end

--[=[
	Read an unsigned 16-bit integer from the buffer.
]=]
function BufferReader:ReadUInt16(): number
	self:_assertSize(self._cursor + 2)
	local n = buffer.readu16(self._buffer, self._cursor)
	self._cursor += 2
	return n
end

--[=[
	Read a signed 32-bit integer from the buffer.
]=]
function BufferReader:ReadInt32(): number
	self:_assertSize(self._cursor + 4)
	local n = buffer.readi32(self._buffer, self._cursor)
	self._cursor += 4
	return n
end

--[=[
	Read an unsigned 32-bit integer from the buffer.
]=]
function BufferReader:ReadUInt32(): number
	self:_assertSize(self._cursor + 4)
	local n = buffer.readu32(self._buffer, self._cursor)
	self._cursor += 4
	return n
end

--[=[
	Read a 32-bit single-precision float from the buffer.
]=]
function BufferReader:ReadFloat32(): number
	self:_assertSize(self._cursor + 4)
	local n = buffer.readf32(self._buffer, self._cursor)
	self._cursor += 4
	return n
end

--[=[
	Read a 64-bit double-precision float from the buffer.
]=]
function BufferReader:ReadFloat64(): number
	self:_assertSize(self._cursor + 8)
	local n = buffer.readf64(self._buffer, self._cursor)
	self._cursor += 8
	return n
end

--[=[
	Read a boolean from the buffer.
]=]
function BufferReader:ReadBool(): boolean
	local n = self:ReadUInt8()
	return n == 1
end

--[=[
	Read a string from the buffer.

	:::info
	This assumes the string was written using the `BufferWriter:WriteString()`
	method, which stores an extra integer to mark the size of the string.
	:::
]=]
function BufferReader:ReadString(): string
	local strLen = self:ReadUInt32()
	self:_assertSize(self._cursor + strLen)
	local s = buffer.readstring(self._buffer, self._cursor, strLen)
	self._cursor += strLen
	return s
end

--[=[
	Read a string from the buffer.

	:::info
	This assumes the string was written using the `BufferWriter:WriteStringRaw()`.
	:::
]=]
function BufferReader:ReadStringRaw(length: number): string
	length = math.max(0, math.floor(length))
	self:_assertSize(self._cursor + length)
	local s = buffer.readstring(self._buffer, self._cursor, length)
	self._cursor += length
	return s
end

--[=[
	Read a DataType from the buffer.

	```lua
	local cframe = reader:ReadDataType(CFrame)
	```
]=]
function BufferReader:ReadDataType<T>(dataType: T): T
	local name = DataTypeBuffer.DataTypesToString[dataType]
	if not name then
		error("unsupported data type", 2)
	end

	local readWrite = DataTypeBuffer.ReadWrite[name]
	return readWrite.read(self)
end

--[=[
	Sets the position of the cursor.
]=]
function BufferReader:SetCursor(position: number)
	position = math.floor(position)
	if position < 0 or position > self._size then
		error(`cursor position {position} out of range [0, {self._size}]`, 3)
	end

	self._cursor = position
end

--[=[
	Returns the position of the cursor.
]=]
function BufferReader:GetCursor(): number
	return self._cursor
end

--[=[
	Resets the position of the cursor.
]=]
function BufferReader:ResetCursor()
	self._cursor = 0
end

--[=[
	Returns the size of the buffer.
]=]
function BufferReader:GetSize(): number
	return self._size
end

--[=[
	Returns the `buffer` object.
]=]
function BufferReader:GetBuffer(): buffer
	return self._buffer
end

function BufferReader:__tostring()
	return "BufferReader"
end

return BufferReader


--!native

local MAX_SIZE = 1073741824

local DataTypeBuffer = require(script.Parent.DataTypeBuffer)
local Types = require(script.Parent.Types)

--[=[
	@class BufferWriter

	A BufferWriter is an abstraction wrapper for `buffer` objects
	that provides a convenient way of writing data to buffers.

	The internal buffer is automatically resized to fit the data
	that is being written.
]=]
local BufferWriter = {}
BufferWriter.__index = BufferWriter

function BufferWriter.new(initialSize: number?): Types.BufferWriter
	local size = if typeof(initialSize) == "number" then math.clamp(initialSize, 0, MAX_SIZE) else 0

	local self = setmetatable({
		_buffer = buffer.create(size),
		_cursor = 0,
		_size = 0,
	}, BufferWriter)

	return self
end

function BufferWriter:_resizeUpTo(desiredSize: number)
	if desiredSize > MAX_SIZE then
		error(`cannot resize buffer to {desiredSize} bytes (max size: {MAX_SIZE} bytes)`, 3)
	end

	self._size = math.max(self._size, desiredSize)

	if desiredSize <= buffer.len(self._buffer) then
		return
	end

	local newSize = desiredSize

	local powerOfTwo = math.log(desiredSize, 2)
	if math.floor(powerOfTwo) ~= powerOfTwo then
		newSize = 2 ^ (math.floor(powerOfTwo) + 1)
	end

	local oldBuffer = self._buffer
	local newBuffer = buffer.create(newSize)

	buffer.copy(newBuffer, 0, oldBuffer, 0)

	self._buffer = newBuffer
end

--[=[
	Write a signed 8-bit integer to the buffer.
]=]
function BufferWriter:WriteInt8(int8: number)
	self:_resizeUpTo(self._cursor + 1)
	buffer.writei8(self._buffer, self._cursor, int8)
	self._cursor += 1
end

--[=[
	Write an unsigned 8-bit integer to the buffer.
]=]
function BufferWriter:WriteUInt8(uint8: number)
	self:_resizeUpTo(self._cursor + 1)
	buffer.writeu8(self._buffer, self._cursor, uint8)
	self._cursor += 1
end

--[=[
	Write a signed 16-bit integer to the buffer.
]=]
function BufferWriter:WriteInt16(int16: number)
	self:_resizeUpTo(self._cursor + 2)
	buffer.writei16(self._buffer, self._cursor, int16)
	self._cursor += 2
end

--[=[
	Write an unsigned 16-bit integer to the buffer.
]=]
function BufferWriter:WriteUInt16(uint16: number)
	self:_resizeUpTo(self._cursor + 2)
	buffer.writeu16(self._buffer, self._cursor, uint16)
	self._cursor += 2
end

--[=[
	Write a signed 32-bit integer to the buffer.
]=]
function BufferWriter:WriteInt32(int32: number)
	self:_resizeUpTo(self._cursor + 4)
	buffer.writei32(self._buffer, self._cursor, int32)
	self._cursor += 4
end

--[=[
	Write an unsigned 32-bit integer to the buffer.
]=]
function BufferWriter:WriteUInt32(uint32: number)
	self:_resizeUpTo(self._cursor + 4)
	buffer.writeu32(self._buffer, self._cursor, uint32)
	self._cursor += 4
end

--[=[
	Write a 32-bit single-precision float to the buffer.
]=]
function BufferWriter:WriteFloat32(f32: number)
	self:_resizeUpTo(self._cursor + 4)
	buffer.writef32(self._buffer, self._cursor, f32)
	self._cursor += 4
end

--[=[
	Write a 64-bit double-precision float to the buffer.
]=]
function BufferWriter:WriteFloat64(f64: number)
	self:_resizeUpTo(self._cursor + 8)
	buffer.writef64(self._buffer, self._cursor, f64)
	self._cursor += 8
end

--[=[
	Write a boolean to the buffer.
]=]
function BufferWriter:WriteBool(bool: boolean)
	self:WriteUInt8(if bool then 1 else 0)
end

--[=[
	Write a string to the buffer. An optional `length` argument can
	be provided to limit the number of bytes read from the string.

	:::info
	An extra 32-bit integer is stored before the string to mark the
	length of the string. If the length of the string is always
	known, it is more memory-efficient to use `WriteStringRaw`.
	:::
]=]
function BufferWriter:WriteString(str: string, length: number?)
	local len = if length then math.min(#str, length) else #str
	local size = len + 4
	self:_resizeUpTo(self._cursor + size)
	buffer.writeu32(self._buffer, self._cursor, len)
	buffer.writestring(self._buffer, self._cursor + 4, str, length)
	self._cursor += size
end

--[=[
	Write a string to the buffer. An optional `length` argument can
	be provided to limit the number of bytes read from the string.

	:::info
	The length of the string must be known to the reader in order to
	read this string from the buffer, as the length of the string is
	not stored in the buffer. For strings of dynamic/unknown length,
	use `WriteString` instead.
	:::
]=]
function BufferWriter:WriteStringRaw(str: string, length: number?)
	local len = if length then math.min(#str, length) else #str
	self:_resizeUpTo(self._cursor + len)
	buffer.writestring(self._buffer, self._cursor, str, length)
	self._cursor += len
end

--[=[
	Write a DataType to the buffer.

	```lua
	writer:WriteDataType(CFrame.new(0, 10, 5))
	```
]=]
function BufferWriter:WriteDataType(value: any)
	local t = typeof(value)
	local readWrite = DataTypeBuffer.ReadWrite[t]
	if not readWrite then
		error(`unsupported data type "{t}"`, 2)
	end
	readWrite.write(self, value)
end

--[=[
	Shrinks the capacity of the buffer to the current data size.
]=]
function BufferWriter:Shrink()
	if self._size == buffer.len(self._buffer) then
		return
	end

	local oldBuffer = self._buffer
	local newBuffer = buffer.create(self._size)

	buffer.copy(newBuffer, 0, oldBuffer, 0, self._size)

	self._buffer = newBuffer
end

--[=[
	Returns the current data size of the buffer. This is _not_ necessarily
	equal to the capacity of the buffer.
]=]
function BufferWriter:GetSize(): number
	return self._size
end

--[=[
	Returns the current capacity of the buffer. This is the length of the
	internal buffer, which is usually not the same as the length of the stored
	data.

	The buffer capacity automatically grows as data is added.
]=]
function BufferWriter:GetCapacity(): number
	return buffer.len(self._buffer)
end

--[=[
	Sets the position of the cursor.
]=]
function BufferWriter:SetCursor(position: number)
	position = math.floor(position)
	if position < 0 or position > self._size then
		error(`cursor position {position} out of range [0, {self._size}]`, 3)
	end

	self._cursor = position
end

--[=[
	Gets the position of the cursor.
]=]
function BufferWriter:GetCursor(): number
	return self._cursor
end

--[=[
	Resets the position of the cursor.
]=]
function BufferWriter:ResetCursor()
	self._cursor = 0
end

--[=[
	Returns the `buffer` object.
]=]
function BufferWriter:GetBuffer(): buffer
	return self._buffer
end

--[=[
	Returns the string version of the internal buffer.
]=]
function BufferWriter:ToString(): string
	return buffer.tostring(self._buffer)
end

function BufferWriter:__tostring()
	return "BufferWriter"
end

return BufferWriter


--!native

local Types = require(script.Parent.Types)

type ReadWritePair = {
	read: <T>(reader: Types.BufferReader) -> T,
	write: <T>(writer: Types.BufferWriter, value: T) -> (),
}

local DataTypeBuffer = {}

DataTypeBuffer.DataTypesToString = {
	[BrickColor] = "BrickColor",
	[CFrame] = "CFrame",
	[Color3] = "Color3",
	[DateTime] = "DateTime",
	[Ray] = "Ray",
	[Rect] = "Rect",
	[Region3] = "Region3",
	[Region3int16] = "Region3int16",
	[UDim] = "UDim",
	[UDim2] = "UDim2",
	[Vector2] = "Vector2",
	[Vector3] = "Vector3",
	[Vector2int16] = "Vector2int16",
	[Vector3int16] = "Vector3int16",
}

DataTypeBuffer.ReadWrite = {} :: { [string]: ReadWritePair }

DataTypeBuffer.ReadWrite.BrickColor = {
	write = function(writer: Types.BufferWriter, brickColor: BrickColor)
		writer:WriteUInt16(brickColor.Number)
	end,

	read = function(reader: Types.BufferReader): BrickColor
		local number = reader:ReadUInt16()
		return BrickColor.new(number)
	end,
}

DataTypeBuffer.ReadWrite.CFrame = {
	write = function(writer: Types.BufferWriter, cf: CFrame)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, cf.Position)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, cf.XVector)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, cf.YVector)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, cf.ZVector)
	end,

	read = function(reader: Types.BufferReader): CFrame
		local pos = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		local vx = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		local vy = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		local vz = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		return CFrame.fromMatrix(pos, vx, vy, vz)
	end,
}

DataTypeBuffer.ReadWrite.Color3 = {
	write = function(writer: Types.BufferWriter, c: Color3)
		writer:WriteFloat32(c.R)
		writer:WriteFloat32(c.G)
		writer:WriteFloat32(c.B)
	end,

	read = function(reader: Types.BufferReader): Color3
		local r = reader:ReadFloat32()
		local g = reader:ReadFloat32()
		local b = reader:ReadFloat32()
		return Color3.new(r, g, b)
	end,
}

DataTypeBuffer.ReadWrite.DateTime = {
	write = function(writer: Types.BufferWriter, dt: DateTime)
		writer:WriteFloat64(dt.UnixTimestampMillis)
	end,

	read = function(reader: Types.BufferReader): DateTime
		local millis = reader:ReadFloat64()
		return DateTime.fromUnixTimestampMillis(millis)
	end,
}

DataTypeBuffer.ReadWrite.Ray = {
	write = function(writer: Types.BufferWriter, ray: Ray)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, ray.Origin)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, ray.Direction)
	end,

	read = function(reader: Types.BufferReader): Ray
		local origin = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		local direction = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		return Ray.new(origin, direction)
	end,
}

DataTypeBuffer.ReadWrite.Rect = {
	write = function(writer: Types.BufferWriter, rect: Rect)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, rect.Min)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, rect.Max)
	end,

	read = function(reader: Types.BufferReader): Rect
		local min = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		local max = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		return Rect.new(min, max)
	end,
}

DataTypeBuffer.ReadWrite.Region3 = {
	write = function(writer: Types.BufferWriter, region3: Region3)
		local pos = region3.CFrame.Position
		local sizeHalf = region3.Size * 0.5
		local min = pos - sizeHalf
		local max = pos + sizeHalf
		DataTypeBuffer.ReadWrite.Vector3.write(writer, min)
		DataTypeBuffer.ReadWrite.Vector3.write(writer, max)
	end,

	read = function(reader: Types.BufferReader): Region3
		local min = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		local max = DataTypeBuffer.ReadWrite.Vector3.read(reader)
		return Region3.new(min, max)
	end,
}

DataTypeBuffer.ReadWrite.Region3int16 = {
	write = function(writer: Types.BufferWriter, region3int16: Region3int16)
		DataTypeBuffer.ReadWrite.Vector3int16.write(writer, region3int16.Min)
		DataTypeBuffer.ReadWrite.Vector3int16.write(writer, region3int16.Max)
	end,

	read = function(reader: Types.BufferReader): Region3int16
		local min = DataTypeBuffer.ReadWrite.Vector3int16.read(reader)
		local max = DataTypeBuffer.ReadWrite.Vector3int16.read(reader)
		return Region3int16.new(min, max)
	end,
}

DataTypeBuffer.ReadWrite.UDim = {
	write = function(writer: Types.BufferWriter, udim: UDim)
		writer:WriteFloat32(udim.Scale)
		writer:WriteInt32(udim.Offset)
	end,

	read = function(reader: Types.BufferReader): UDim
		local scale = reader:ReadFloat32()
		local offset = reader:ReadInt32()
		return UDim.new(scale, offset)
	end,
}

DataTypeBuffer.ReadWrite.UDim2 = {
	write = function(writer: Types.BufferWriter, udim2: UDim2)
		DataTypeBuffer.ReadWrite.UDim.write(writer, udim2.X)
		DataTypeBuffer.ReadWrite.UDim.write(writer, udim2.Y)
	end,

	read = function(reader: Types.BufferReader): UDim2
		local x = DataTypeBuffer.ReadWrite.UDim.read(reader)
		local y = DataTypeBuffer.ReadWrite.UDim.read(reader)
		return UDim2.new(x, y)
	end,
}

DataTypeBuffer.ReadWrite.Vector2 = {
	write = function(writer: Types.BufferWriter, v2: Vector2)
		writer:WriteFloat32(v2.X)
		writer:WriteFloat32(v2.Y)
	end,

	read = function(reader: Types.BufferReader): Vector2
		local x = reader:ReadFloat32()
		local y = reader:ReadFloat32()
		return Vector2.new(x, y)
	end,
}

DataTypeBuffer.ReadWrite.Vector3 = {
	write = function(writer: Types.BufferWriter, v3: Vector3)
		writer:WriteFloat32(v3.X)
		writer:WriteFloat32(v3.Y)
		writer:WriteFloat32(v3.Z)
	end,

	read = function(reader: Types.BufferReader): Vector3
		local x = reader:ReadFloat32()
		local y = reader:ReadFloat32()
		local z = reader:ReadFloat32()
		return Vector3.new(x, y, z)
	end,
}

DataTypeBuffer.ReadWrite.Vector2int16 = {
	write = function(writer: Types.BufferWriter, v2: Vector2int16)
		writer:WriteInt16(v2.X)
		writer:WriteInt16(v2.Y)
	end,

	read = function(reader: Types.BufferReader): Vector2int16
		local x = reader:ReadInt16()
		local y = reader:ReadInt16()
		return Vector2int16.new(x, y)
	end,
}

DataTypeBuffer.ReadWrite.Vector3int16 = {
	write = function(writer: Types.BufferWriter, v3: Vector3int16)
		writer:WriteInt16(v3.X)
		writer:WriteInt16(v3.Y)
		writer:WriteInt16(v3.Z)
	end,

	read = function(reader: Types.BufferReader): Vector3int16
		local x = reader:ReadInt16()
		local y = reader:ReadInt16()
		local z = reader:ReadInt16()
		return Vector3int16.new(x, y, z)
	end,
}

return DataTypeBuffer


export type DataTypes =
	| BrickColor
	| CFrame
	| Color3
	| DateTime
	| Ray
	| Rect
	| Region3
	| Region3int16
	| UDim
	| UDim2
	| Vector2
	| Vector3
	| Vector2int16
	| Vector3int16

export type BufferReader = {
	ReadInt8: (self: BufferReader) -> number,
	ReadUInt8: (self: BufferReader) -> number,
	ReadInt16: (self: BufferReader) -> number,
	ReadUInt16: (self: BufferReader) -> number,
	ReadInt32: (self: BufferReader) -> number,
	ReadUInt32: (self: BufferReader) -> number,
	ReadFloat32: (self: BufferReader) -> number,
	ReadFloat64: (self: BufferReader) -> number,
	ReadBool: (self: BufferReader) -> boolean,
	ReadString: (self: BufferReader) -> string,
	ReadStringRaw: (self: BufferReader, length: number) -> string,
	ReadDataType: <T>(self: BufferReader, dataType: { new: (...any) -> T }) -> T,
	GetSize: (self: BufferReader) -> number,
	ResetCursor: (self: BufferReader) -> (),
	SetCursor: (self: BufferReader, cursorPosition: number) -> (),
	GetCursor: (self: BufferReader) -> number,
	GetBuffer: (self: BufferReader) -> buffer,
}

export type BufferWriter = {
	WriteInt8: (self: BufferWriter, int8: number) -> (),
	WriteUInt8: (self: BufferWriter, uint8: number) -> (),
	WriteInt16: (self: BufferWriter, int16: number) -> (),
	WriteUInt16: (self: BufferWriter, uint16: number) -> (),
	WriteInt32: (self: BufferWriter, int32: number) -> (),
	WriteUInt32: (self: BufferWriter, uint32: number) -> (),
	WriteFloat32: (self: BufferWriter, f32: number) -> (),
	WriteFloat64: (self: BufferWriter, f64: number) -> (),
	WriteBool: (self: BufferWriter, bool: boolean) -> (),
	WriteString: (self: BufferWriter, str: string, length: number?) -> (),
	WriteStringRaw: (self: BufferWriter, str: string, length: number?) -> (),
	WriteDataType: (self: BufferWriter, data: DataTypes) -> (),
	GetSize: (self: BufferWriter) -> number,
	GetCapacity: (self: BufferWriter) -> number,
	ResetCursor: (self: BufferWriter) -> (),
	SetCursor: (self: BufferWriter, cursorPosition: number) -> (),
	GetCursor: (self: BufferWriter) -> number,
	Shrink: (self: BufferWriter) -> (),
	GetBuffer: (self: BufferWriter) -> buffer,
	ToString: (self: BufferWriter) -> string,
}

return nil


--!native

local BufferReader = require(script.BufferReader)
local BufferWriter = require(script.BufferWriter)

--[=[
	@class BufferUtil
]=]

--[=[
	@within BufferUtil
	@function reader
	@param buf buffer | string
	@return BufferReader

	Creates a BufferReader. The reader can be constructed from either
	a string or a `buffer` object.

	```lua
	local reader = BufferUtil.reader()
	```
]=]

--[=[
	@within BufferUtil
	@function writer
	@param initialCapacity number?
	@return BufferWriter

	Creates a zero-initialized BufferWriter. An initial capacity can
	optionally be set, and is defaulted to `0`.

	```lua
	local writer = BufferUtil.writer()
	```
]=]

return {
	reader = BufferReader.new,
	writer = BufferWriter.new,
}


-- ClientRemoteProperty
-- Stephen Leitnick
-- December 20, 2021

local Promise = require(script.Parent.Parent.Parent.Promise)
local Signal = require(script.Parent.Parent.Parent.Signal)
local ClientRemoteSignal = require(script.Parent.ClientRemoteSignal)
local Types = require(script.Parent.Parent.Types)

--[=[
	@within ClientRemoteProperty
	@prop Changed Signal<any>

	Fires when the property receives an updated value
	from the server.

	```lua
	clientRemoteProperty.Changed:Connect(function(value)
		print("New value", value)
	end)
	```
]=]

--[=[
	@class ClientRemoteProperty
	@client
	Created via `ClientComm:GetProperty()`.
]=]
local ClientRemoteProperty = {}
ClientRemoteProperty.__index = ClientRemoteProperty

function ClientRemoteProperty.new(
	re: RemoteEvent,
	inboundMiddleware: Types.ClientMiddleware?,
	outboudMiddleware: Types.ClientMiddleware?
)
	local self = setmetatable({}, ClientRemoteProperty)
	self._rs = ClientRemoteSignal.new(re, inboundMiddleware, outboudMiddleware)
	self._ready = false
	self._value = nil
	self.Changed = Signal.new()
	self._rs:Fire()

	local resolveOnReadyPromise
	self._readyPromise = Promise.new(function(resolve)
		resolveOnReadyPromise = resolve
	end)
	self._changed = self._rs:Connect(function(value)
		local changed = value ~= self._value
		self._value = value
		if not self._ready then
			self._ready = true
			resolveOnReadyPromise(value)
		end
		if changed then
			self.Changed:Fire(value)
		end
	end)

	return self
end

--[=[
	Gets the value of the property object.

	:::caution
	This value might not be ready right away. Use `OnReady()` or `IsReady()`
	before calling `Get()`. If not ready, this value will return `nil`.
	:::
]=]
function ClientRemoteProperty:Get(): any
	return self._value
end

--[=[
	@return Promise<any>
	Returns a Promise which resolves once the property object is
	ready to be used. The resolved promise will also contain the
	value of the property.

	```lua
	-- Use andThen clause:
	clientRemoteProperty:OnReady():andThen(function(initialValue)
		print(initialValue)
	end)

	-- Use await:
	local success, initialValue = clientRemoteProperty:OnReady():await()
	if success then
		print(initialValue)
	end
	```
]=]
function ClientRemoteProperty:OnReady()
	return self._readyPromise
end

--[=[
	Returns `true` if the property object is ready to be
	used. In other words, it has successfully gained
	connection to the server-side version and has synced
	in the initial value.

	```lua
	if clientRemoteProperty:IsReady() then
		local value = clientRemoteProperty:Get()
	end
	```
]=]
function ClientRemoteProperty:IsReady(): boolean
	return self._ready
end

--[=[
	@param observer (any) -> nil
	@return Connection
	Observes the value of the property. The observer will
	be called right when the value is first ready, and
	every time the value changes. This is safe to call
	immediately (i.e. no need to use `IsReady` or `OnReady`
	before using this method).

	Observing is essentially listening to `Changed`, but
	also sends the initial value right away (or at least
	once `OnReady` is completed).

	```lua
	local function ObserveValue(value)
		print(value)
	end

	clientRemoteProperty:Observe(ObserveValue)
	```
]=]
function ClientRemoteProperty:Observe(observer: (any) -> ())
	if self._ready then
		task.defer(observer, self._value)
	end
	return self.Changed:Connect(observer)
end

--[=[
	Destroys the ClientRemoteProperty object.
]=]
function ClientRemoteProperty:Destroy()
	self._rs:Destroy()
	if self._readyPromise then
		self._readyPromise:cancel()
	end
	if self._changed then
		self._changed:Disconnect()
	end
	self.Changed:Destroy()
end

return ClientRemoteProperty


-- ClientRemoteSignal
-- Stephen Leitnick
-- December 20, 2021

local Signal = require(script.Parent.Parent.Parent.Signal)
local Types = require(script.Parent.Parent.Types)

--[=[
	@class ClientRemoteSignal
	@client
	Created via `ClientComm:GetSignal()`.
]=]
local ClientRemoteSignal = {}
ClientRemoteSignal.__index = ClientRemoteSignal

--[=[
	@within ClientRemoteSignal
	@interface Connection
	.Disconnect () -> ()

	Represents a connection.
]=]

function ClientRemoteSignal.new(
	re: RemoteEvent | UnreliableRemoteEvent,
	inboundMiddleware: Types.ClientMiddleware?,
	outboudMiddleware: Types.ClientMiddleware?
)
	local self = setmetatable({}, ClientRemoteSignal)
	self._re = re
	if outboudMiddleware and #outboudMiddleware > 0 then
		self._hasOutbound = true
		self._outbound = outboudMiddleware
	else
		self._hasOutbound = false
	end
	if inboundMiddleware and #inboundMiddleware > 0 then
		self._directConnect = false
		self._signal = Signal.new()
		self._reConn = self._re.OnClientEvent:Connect(function(...)
			local args = table.pack(...)
			for _, middlewareFunc in inboundMiddleware do
				local middlewareResult = table.pack(middlewareFunc(args))
				if not middlewareResult[1] then
					return
				end
				args.n = #args
			end
			self._signal:Fire(table.unpack(args, 1, args.n))
		end)
	else
		self._directConnect = true
	end
	return self
end

function ClientRemoteSignal:_processOutboundMiddleware(...: any)
	local args = table.pack(...)
	for _, middlewareFunc in self._outbound do
		local middlewareResult = table.pack(middlewareFunc(args))
		if not middlewareResult[1] then
			return table.unpack(middlewareResult, 2, middlewareResult.n)
		end
		args.n = #args
	end
	return table.unpack(args, 1, args.n)
end

--[=[
	@param fn (...: any) -> ()
	@return Connection
	Connects a function to the remote signal. The function will be
	called anytime the equivalent server-side RemoteSignal is
	fired at this specific client that created this client signal.
]=]
function ClientRemoteSignal:Connect(fn: (...any) -> ())
	if self._directConnect then
		return self._re.OnClientEvent:Connect(fn)
	else
		return self._signal:Connect(fn)
	end
end

--[=[
	Fires the equivalent server-side signal with the given arguments.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware before being
	sent to the server.
	:::
]=]
function ClientRemoteSignal:Fire(...: any)
	if self._hasOutbound then
		self._re:FireServer(self:_processOutboundMiddleware(...))
	else
		self._re:FireServer(...)
	end
end

--[=[
	Destroys the ClientRemoteSignal object.
]=]
function ClientRemoteSignal:Destroy()
	if self._signal then
		self._signal:Destroy()
	end
end

return ClientRemoteSignal


local Util = require(script.Parent.Util)
local Types = require(script.Parent.Types)
local Promise = require(script.Parent.Parent.Promise)
local ClientRemoteSignal = require(script.ClientRemoteSignal)
local ClientRemoteProperty = require(script.ClientRemoteProperty)

local Client = {}

function Client.GetFunction(
	parent: Instance,
	name: string,
	usePromise: boolean,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	assert(not Util.IsServer, "GetFunction must be called from the client")
	local folder = Util.GetCommSubFolder(parent, "RF"):Expect("Failed to get Comm RF folder")
	local rf = folder:WaitForChild(name, Util.WaitForChildTimeout)
	assert(rf ~= nil, "Failed to find RemoteFunction: " .. name)
	local hasInbound = type(inboundMiddleware) == "table" and #inboundMiddleware > 0
	local hasOutbound = type(outboundMiddleware) == "table" and #outboundMiddleware > 0
	local function ProcessOutbound(args)
		for _, middlewareFunc in ipairs(outboundMiddleware) do
			local middlewareResult = table.pack(middlewareFunc(args))
			if not middlewareResult[1] then
				return table.unpack(middlewareResult, 2, middlewareResult.n)
			end
			args.n = #args
		end
		return table.unpack(args, 1, args.n)
	end
	if hasInbound then
		if usePromise then
			return function(...)
				local args = table.pack(...)
				return Promise.new(function(resolve, reject)
					local success, res = pcall(function()
						if hasOutbound then
							return table.pack(rf:InvokeServer(ProcessOutbound(args)))
						else
							return table.pack(rf:InvokeServer(table.unpack(args, 1, args.n)))
						end
					end)
					if success then
						for _, middlewareFunc in ipairs(inboundMiddleware) do
							local middlewareResult = table.pack(middlewareFunc(res))
							if not middlewareResult[1] then
								return table.unpack(middlewareResult, 2, middlewareResult.n)
							end
							res.n = #res
						end
						resolve(table.unpack(res, 1, res.n))
					else
						reject(res)
					end
				end)
			end
		else
			return function(...)
				local res
				if hasOutbound then
					res = table.pack(rf:InvokeServer(ProcessOutbound(table.pack(...))))
				else
					res = table.pack(rf:InvokeServer(...))
				end
				for _, middlewareFunc in ipairs(inboundMiddleware) do
					local middlewareResult = table.pack(middlewareFunc(res))
					if not middlewareResult[1] then
						return table.unpack(middlewareResult, 2, middlewareResult.n)
					end
					res.n = #res
				end
				return table.unpack(res, 1, res.n)
			end
		end
	else
		if usePromise then
			return function(...)
				local args = table.pack(...)
				return Promise.new(function(resolve, reject)
					local success, res = pcall(function()
						if hasOutbound then
							return table.pack(rf:InvokeServer(ProcessOutbound(args)))
						else
							return table.pack(rf:InvokeServer(table.unpack(args, 1, args.n)))
						end
					end)
					if success then
						resolve(table.unpack(res, 1, res.n))
					else
						reject(res)
					end
				end)
			end
		else
			if hasOutbound then
				return function(...)
					return rf:InvokeServer(ProcessOutbound(table.pack(...)))
				end
			else
				return function(...)
					return rf:InvokeServer(...)
				end
			end
		end
	end
end

function Client.GetSignal(
	parent: Instance,
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	assert(not Util.IsServer, "GetSignal must be called from the client")
	local folder = Util.GetCommSubFolder(parent, "RE"):Expect("Failed to get Comm RE folder")
	local re = folder:WaitForChild(name, Util.WaitForChildTimeout)
	assert(re ~= nil, "Failed to find RemoteEvent: " .. name)
	return ClientRemoteSignal.new(re, inboundMiddleware, outboundMiddleware)
end

function Client.GetProperty(
	parent: Instance,
	name: string,
	inboundMiddleware: Types.ClientMiddleware?,
	outboundMiddleware: Types.ClientMiddleware?
)
	assert(not Util.IsServer, "GetProperty must be called from the client")
	local folder = Util.GetCommSubFolder(parent, "RP"):Expect("Failed to get Comm RP folder")
	local re = folder:WaitForChild(name, Util.WaitForChildTimeout)
	assert(re ~= nil, "Failed to find RemoteEvent for RemoteProperty: " .. name)
	return ClientRemoteProperty.new(re, inboundMiddleware, outboundMiddleware)
end

return Client


-- RemoteSignal
-- Stephen Leitnick
-- December 20, 2021

local Players = game:GetService("Players")

local Signal = require(script.Parent.Parent.Parent.Signal)
local Types = require(script.Parent.Parent.Types)

--[=[
	@class RemoteSignal
	@server
	Created via `ServerComm:CreateSignal()`.
]=]
local RemoteSignal = {}
RemoteSignal.__index = RemoteSignal

--[=[
	@within RemoteSignal
	@interface Connection
	.Disconnect () -> nil
	.Connected boolean

	Represents a connection.
]=]

function RemoteSignal.new(
	parent: Instance,
	name: string,
	unreliable: boolean?,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	local self = setmetatable({}, RemoteSignal)
	self._re = if unreliable == true then Instance.new("UnreliableRemoteEvent") else Instance.new("RemoteEvent")
	self._re.Name = name
	self._re.Parent = parent
	if outboundMiddleware and #outboundMiddleware > 0 then
		self._hasOutbound = true
		self._outbound = outboundMiddleware
	else
		self._hasOutbound = false
	end
	if inboundMiddleware and #inboundMiddleware > 0 then
		self._directConnect = false
		self._signal = Signal.new()
		self._re.OnServerEvent:Connect(function(player, ...)
			local args = table.pack(...)
			for _, middlewareFunc in inboundMiddleware do
				local middlewareResult = table.pack(middlewareFunc(player, args))
				if not middlewareResult[1] then
					return
				end
				args.n = #args
			end
			self._signal:Fire(player, table.unpack(args, 1, args.n))
		end)
	else
		self._directConnect = true
	end
	return self
end

--[=[
	@return boolean
	Returns `true` if the underlying RemoteSignal is bound to an
	UnreliableRemoteEvent object.
]=]
function RemoteSignal:IsUnreliable(): boolean
	return self._re:IsA("UnreliableRemoteEvent")
end

--[=[
	@param fn (player: Player, ...: any) -> nil -- The function to connect
	@return Connection
	Connect a function to the signal. Anytime a matching ClientRemoteSignal
	on a client fires, the connected function will be invoked with the
	arguments passed by the client.
]=]
function RemoteSignal:Connect(fn)
	if self._directConnect then
		return self._re.OnServerEvent:Connect(fn)
	else
		return self._signal:Connect(fn)
	end
end

function RemoteSignal:_processOutboundMiddleware(player: Player?, ...: any)
	if not self._hasOutbound then
		return ...
	end
	local args = table.pack(...)
	for _, middlewareFunc in self._outbound do
		local middlewareResult = table.pack(middlewareFunc(player, args))
		if not middlewareResult[1] then
			return table.unpack(middlewareResult, 2, middlewareResult.n)
		end
		args.n = #args
	end
	return table.unpack(args, 1, args.n)
end

--[=[
	@param player Player -- The target client
	@param ... any -- Arguments passed to the client
	Fires the signal at the specified client with any arguments.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::
]=]
function RemoteSignal:Fire(player: Player, ...: any)
	self._re:FireClient(player, self:_processOutboundMiddleware(player, ...))
end

--[=[
	@param ... any
	Fires the signal at _all_ clients with any arguments.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::
]=]
function RemoteSignal:FireAll(...: any)
	self._re:FireAllClients(self:_processOutboundMiddleware(nil, ...))
end

--[=[
	@param ignorePlayer Player -- The client to ignore
	@param ... any -- Arguments passed to the other clients
	Fires the signal to all clients _except_ the specified
	client.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::
]=]
function RemoteSignal:FireExcept(ignorePlayer: Player, ...: any)
	self:FireFilter(function(plr)
		return plr ~= ignorePlayer
	end, ...)
end

--[=[
	@param predicate (player: Player, argsFromFire: ...) -> boolean
	@param ... any -- Arguments to pass to the clients (and to the predicate)
	Fires the signal at any clients that pass the `predicate`
	function test. This can be used to fire signals with much
	more control logic.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::

	:::caution Predicate Before Middleware
	The arguments sent to the predicate are sent _before_ getting
	transformed by any middleware.
	:::

	```lua
	-- Fire signal to players of the same team:
	remoteSignal:FireFilter(function(player)
		return player.Team.Name == "Best Team"
	end)
	```
]=]
function RemoteSignal:FireFilter(predicate: (Player, ...any) -> boolean, ...: any)
	for _, player in Players:GetPlayers() do
		if predicate(player, ...) then
			self._re:FireClient(player, self:_processOutboundMiddleware(nil, ...))
		end
	end
end

--[=[
	Fires a signal at the clients within the `players` table. This is
	useful when signals need to fire for a specific set of players.

	For more complex firing, see `FireFilter`.

	:::note Outbound Middleware
	All arguments pass through any outbound middleware (if any)
	before being sent to the clients.
	:::

	```lua
	local players = {somePlayer1, somePlayer2, somePlayer3}
	remoteSignal:FireFor(players, "Hello, players!")
	```
]=]
function RemoteSignal:FireFor(players: { Player }, ...: any)
	for _, player in players do
		self._re:FireClient(player, self:_processOutboundMiddleware(nil, ...))
	end
end

--[=[
	Destroys the RemoteSignal object.
]=]
function RemoteSignal:Destroy()
	self._re:Destroy()
	if self._signal then
		self._signal:Destroy()
	end
end

return RemoteSignal


local RemoteProperty = require(script.RemoteProperty)
local RemoteSignal = require(script.RemoteSignal)
local Types = require(script.Parent.Types)
local Util = require(script.Parent.Util)

local Server = {}

--[=[
	@within Comm
	@prop ServerComm ServerComm
]=]
--[=[
	@within Comm
	@prop ClientComm ClientComm
]=]

--[=[
	@within Comm
	@private
	@interface Server
	.BindFunction (parent: Instance, name: string, fn: FnBind, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteFunction
	.WrapMethod (parent: Instance, tbl: table, name: string, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteFunction
	.CreateSignal (parent: Instance, name: string, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteSignal
	.CreateProperty (parent: Instance, name: string, value: any, inboundMiddleware: ServerMiddleware?, outboundMiddleware: ServerMiddleware?): RemoteProperty
	Server Comm
]=]
--[=[
	@within Comm
	@private
	@interface Client
	.GetFunction (parent: Instance, name: string, usePromise: boolean, inboundMiddleware: ClientMiddleware?, outboundMiddleware: ClientMiddleware?): (...: any) -> any
	.GetSignal (parent: Instance, name: string, inboundMiddleware: ClientMiddleware?, outboundMiddleware: ClientMiddleware?): ClientRemoteSignal
	.GetProperty (parent: Instance, name: string, inboundMiddleware: ClientMiddleware?, outboundMiddleware: ClientMiddleware?): ClientRemoteProperty
	Client Comm
]=]

function Server.BindFunction(
	parent: Instance,
	name: string,
	func: Types.FnBind,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
): RemoteFunction
	assert(Util.IsServer, "BindFunction must be called from the server")
	local folder = Util.GetCommSubFolder(parent, "RF"):Expect("Failed to get Comm RF folder")
	local rf = Instance.new("RemoteFunction")
	rf.Name = name
	local hasInbound = type(inboundMiddleware) == "table" and #inboundMiddleware > 0
	local hasOutbound = type(outboundMiddleware) == "table" and #outboundMiddleware > 0
	local function ProcessOutbound(player, ...)
		local args = table.pack(...)
		for _, middlewareFunc in ipairs(outboundMiddleware) do
			local middlewareResult = table.pack(middlewareFunc(player, args))
			if not middlewareResult[1] then
				return table.unpack(middlewareResult, 2, middlewareResult.n)
			end
			args.n = #args
		end
		return table.unpack(args, 1, args.n)
	end
	if hasInbound and hasOutbound then
		local function OnServerInvoke(player, ...)
			local args = table.pack(...)
			for _, middlewareFunc in ipairs(inboundMiddleware) do
				local middlewareResult = table.pack(middlewareFunc(player, args))
				if not middlewareResult[1] then
					return table.unpack(middlewareResult, 2, middlewareResult.n)
				end
				args.n = #args
			end
			return ProcessOutbound(player, func(player, table.unpack(args, 1, args.n)))
		end
		rf.OnServerInvoke = OnServerInvoke
	elseif hasInbound then
		local function OnServerInvoke(player, ...)
			local args = table.pack(...)
			for _, middlewareFunc in ipairs(inboundMiddleware) do
				local middlewareResult = table.pack(middlewareFunc(player, args))
				if not middlewareResult[1] then
					return table.unpack(middlewareResult, 2, middlewareResult.n)
				end
				args.n = #args
			end
			return func(player, table.unpack(args, 1, args.n))
		end
		rf.OnServerInvoke = OnServerInvoke
	elseif hasOutbound then
		local function OnServerInvoke(player, ...)
			return ProcessOutbound(player, func(player, ...))
		end
		rf.OnServerInvoke = OnServerInvoke
	else
		rf.OnServerInvoke = func
	end
	rf.Parent = folder
	return rf
end

function Server.WrapMethod(
	parent: Instance,
	tbl: {},
	name: string,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
): RemoteFunction
	assert(Util.IsServer, "WrapMethod must be called from the server")
	local fn = tbl[name]
	assert(type(fn) == "function", "Value at index " .. name .. " must be a function; got " .. type(fn))
	return Server.BindFunction(parent, name, function(...)
		return fn(tbl, ...)
	end, inboundMiddleware, outboundMiddleware)
end

function Server.CreateSignal(
	parent: Instance,
	name: string,
	reliable: boolean?,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	assert(Util.IsServer, "CreateSignal must be called from the server")
	local folder = Util.GetCommSubFolder(parent, "RE"):Expect("Failed to get Comm RE folder")
	local rs = RemoteSignal.new(folder, name, reliable, inboundMiddleware, outboundMiddleware)
	return rs
end

function Server.CreateProperty(
	parent: Instance,
	name: string,
	initialValue: any,
	inboundMiddleware: Types.ServerMiddleware?,
	outboundMiddleware: Types.ServerMiddleware?
)
	assert(Util.IsServer, "CreateProperty must be called from the server")
	local folder = Util.GetCommSubFolder(parent, "RP"):Expect("Failed to get Comm RP folder")
	local rp = RemoteProperty.new(folder, name, initialValue, inboundMiddleware, outboundMiddleware)
	return rp
end

return Server


-- Types
-- Stephen Leitnick
-- December 20, 2021

export type Args = {
	n: number,
	[any]: any,
}

export type FnBind = (Instance, ...any) -> ...any

export type ServerMiddlewareFn = (Instance, Args) -> (boolean, ...any)
export type ServerMiddleware = { ServerMiddlewareFn }

export type ClientMiddlewareFn = (Args) -> (boolean, ...any)
export type ClientMiddleware = { ClientMiddlewareFn }

return nil


local RunService = game:GetService("RunService")

local Option = require(script.Parent.Parent.Option)

local Util = {}

Util.IsServer = RunService:IsServer()
Util.WaitForChildTimeout = 60
Util.DefaultCommFolderName = "__comm__"
Util.None = newproxy()

function Util.GetCommSubFolder(parent: Instance, subFolderName: string): Option.Option
	local subFolder: Instance = nil
	if Util.IsServer then
		subFolder = parent:FindFirstChild(subFolderName)
		if not subFolder then
			subFolder = Instance.new("Folder")
			subFolder.Name = subFolderName
			subFolder.Parent = parent
		end
	else
		subFolder = parent:WaitForChild(subFolderName, Util.WaitForChildTimeout)
	end
	return Option.Wrap(subFolder)
end

return Util


-- Comm
-- Stephen Leitnick
-- August 05, 2021

--[=[
	@class Comm
	Remote communication library.

	This exposes the raw functions that are used by the `ServerComm` and `ClientComm` classes.
	Those two classes should be preferred over accessing the functions directly through this
	Comm library.

	```lua
	-- Server
	local ServerComm = require(ReplicatedStorage.Packages.Comm).ServerComm
	local serverComm = ServerComm.new(somewhere, "MyComm")
	serverComm:BindFunction("Hello", function(player: Player)
		return "Hi"
	end)

	-- Client
	local ClientComm = require(ReplicatedStorage.Packages.Comm).ClientComm
	local clientComm = ClientComm.new(somewhere, false, "MyComm")
	local comm = clientComm:BuildObject()
	print(comm:Hello()) --> Hi
	```
]=]
local Comm = {
	Server = require(script.Server),
	Client = require(script.Client),
	ServerComm = require(script.Server.ServerComm),
	ClientComm = require(script.Client.ClientComm),
}

return Comm


-- Component
-- Stephen Leitnick
-- November 26, 2021

type AncestorList = { Instance }

--[=[
	@type ExtensionFn (component) -> ()
	@within Component
]=]
type ExtensionFn = (any) -> ()

--[=[
	@type ExtensionShouldFn (component) -> boolean
	@within Component
]=]
type ExtensionShouldFn = (any) -> boolean

--[=[
	@interface Extension
	@within Component
	.ShouldExtend ExtensionShouldFn?
	.ShouldConstruct ExtensionShouldFn?
	.Constructing ExtensionFn?
	.Constructed ExtensionFn?
	.Starting ExtensionFn?
	.Started ExtensionFn?
	.Stopping ExtensionFn?
	.Stopped ExtensionFn?

	An extension allows the ability to extend the behavior of
	components. This is useful for adding injection systems or
	extending the behavior of components by wrapping around
	component lifecycle methods.

	The `ShouldConstruct` function can be used to indicate
	if the component should actually be created. This must
	return `true` or `false`. A component with multiple
	`ShouldConstruct` extension functions must have them _all_
	return `true` in order for the component to be constructed.
	The `ShouldConstruct` function runs _before_ all other
	extension functions and component lifecycle methods.

	The `ShouldExtend` function can be used to indicate if
	the extension itself should be used. This can be used in
	order to toggle an extension on/off depending on whatever
	logic is appropriate. If no `ShouldExtend` function is
	provided, the extension will always be used if provided
	as an extension to the component.

	As an example, an extension could be created to simply log
	when the various lifecycle stages run on the component:

	```lua
	local Logger = {}
	function Logger.Constructing(component) print("Constructing", component) end
	function Logger.Constructed(component) print("Constructed", component) end
	function Logger.Starting(component) print("Starting", component) end
	function Logger.Started(component) print("Started", component) end
	function Logger.Stopping(component) print("Stopping", component) end
	function Logger.Stopped(component) print("Stopped", component) end

	local MyComponent = Component.new({Tag = "MyComponent", Extensions = {Logger}})
	```

	Sometimes it is useful for an extension to control whether or
	not a component should be constructed. For instance, if a
	component on the client should only be instantiated for the
	local player, an extension might look like this, assuming the
	instance has an attribute linking it to the player's UserId:
	```lua
	local player = game:GetService("Players").LocalPlayer

	local OnlyLocalPlayer = {}
	function OnlyLocalPlayer.ShouldConstruct(component)
		local ownerId = component.Instance:GetAttribute("OwnerId")
		return ownerId == player.UserId
	end

	local MyComponent = Component.new({Tag = "MyComponent", Extensions = {OnlyLocalPlayer}})
	```

	It can also be useful for an extension itself to turn on/off
	depending on various contexts. For example, let's take the
	Logger from the first example, and only use that extension
	if the bound instance has a Log attribute set to `true`:
	```lua
	function Logger.ShouldExtend(component)
		return component.Instance:GetAttribute("Log") == true
	end
	```
]=]
type Extension = {
	ShouldExtend: ExtensionShouldFn?,
	ShouldConstruct: ExtensionShouldFn?,
	Constructing: ExtensionFn?,
	Constructed: ExtensionFn?,
	Starting: ExtensionFn?,
	Started: ExtensionFn?,
	Stopping: ExtensionFn?,
	Stopped: ExtensionFn?,
}

--[=[
	@interface ComponentConfig
	@within Component
	.Tag string -- CollectionService tag to use
	.Ancestors {Instance}? -- Optional array of ancestors in which components will be started
	.Extensions {Extension}? -- Optional array of extension objects

	Component configuration passed to `Component.new`.

	- If no Ancestors option is included, it defaults to `{workspace, game.Players}`.
	- If no Extensions option is included, it defaults to a blank table `{}`.
]=]
type ComponentConfig = {
	Tag: string,
	Ancestors: AncestorList?,
	Extensions: { Extension }?,
}

--[=[
	@within Component
	@prop Started Signal
	@tag Event
	@tag Component Class

	Fired when a new instance of a component is started.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	MyComponent.Started:Connect(function(component) end)
	```
]=]

--[=[
	@within Component
	@prop Stopped Signal
	@tag Event
	@tag Component Class

	Fired when an instance of a component is stopped.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	MyComponent.Stopped:Connect(function(component) end)
	```
]=]

--[=[
	@tag Component Instance
	@within Component
	@prop Instance Instance

	A reference back to the _Roblox_ instance from within a _component_ instance. When
	a component instance is created, it is bound to a specific Roblox instance, which
	will always be present through the `Instance` property.

	```lua
	MyComponent.Started:Connect(function(component)
		local robloxInstance: Instance = component.Instance
		print("Component is bound to " .. robloxInstance:GetFullName())
	end)
	```
]=]

local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

local Promise = require(script.Parent.Promise)
local Signal = require(script.Parent.Signal)
local Symbol = require(script.Parent.Symbol)
local Trove = require(script.Parent.Trove)

local IS_SERVER = RunService:IsServer()
local DEFAULT_ANCESTORS = { workspace, game:GetService("Players") }
local DEFAULT_TIMEOUT = 60

-- Symbol keys:
local KEY_ANCESTORS = Symbol("Ancestors")
local KEY_INST_TO_COMPONENTS = Symbol("InstancesToComponents")
local KEY_LOCK_CONSTRUCT = Symbol("LockConstruct")
local KEY_COMPONENTS = Symbol("Components")
local KEY_TROVE = Symbol("Trove")
local KEY_EXTENSIONS = Symbol("Extensions")
local KEY_ACTIVE_EXTENSIONS = Symbol("ActiveExtensions")
local KEY_STARTING = Symbol("Starting")
local KEY_STARTED = Symbol("Started")

local renderId = 0
local function NextRenderName(): string
	renderId += 1
	return "ComponentRender" .. tostring(renderId)
end

local function InvokeExtensionFn(component, fnName: string)
	for _, extension in ipairs(component[KEY_ACTIVE_EXTENSIONS]) do
		local fn = extension[fnName]
		if type(fn) == "function" then
			fn(component)
		end
	end
end

local function ShouldConstruct(component): boolean
	for _, extension in ipairs(component[KEY_ACTIVE_EXTENSIONS]) do
		local fn = extension.ShouldConstruct
		if type(fn) == "function" then
			local shouldConstruct = fn(component)
			if not shouldConstruct then
				return false
			end
		end
	end
	return true
end

local function GetActiveExtensions(component, extensionList)
	local activeExtensions = table.create(#extensionList)
	local allActive = true
	for _, extension in ipairs(extensionList) do
		local fn = extension.ShouldExtend
		local shouldExtend = type(fn) ~= "function" or not not fn(component)
		if shouldExtend then
			table.insert(activeExtensions, extension)
		else
			allActive = false
		end
	end
	return if allActive then extensionList else activeExtensions
end

--[=[
	@class Component

	Bind components to Roblox instances using the Component class and CollectionService tags.

	To avoid confusion of terms:
	- `Component` refers to this module.
	- `Component Class` (e.g. `MyComponent` through this documentation) refers to a class created via `Component.new`
	- `Component Instance` refers to an instance of a component class.
	- `Roblox Instance` refers to the Roblox instance to which the component instance is bound.

	Methods and properties are tagged with the above terms to help clarify the level at which they are used.
]=]
local Component = {}
Component.__index = Component

--[=[
	@tag Component
	@param config ComponentConfig
	@return ComponentClass

	Create a new custom Component class.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})
	```

	A full example might look like this:

	```lua
	local MyComponent = Component.new({
		Tag = "MyComponent",
		Ancestors = {workspace},
		Extensions = {Logger}, -- See Logger example within the example for the Extension type
	})

	local AnotherComponent = require(somewhere.AnotherComponent)

	-- Optional if UpdateRenderStepped should use BindToRenderStep:
	MyComponent.RenderPriority = Enum.RenderPriority.Camera.Value

	function MyComponent:Construct()
		self.MyData = "Hello"
	end

	function MyComponent:Start()
		local another = self:GetComponent(AnotherComponent)
		another:DoSomething()
	end

	function MyComponent:Stop()
		self.MyData = "Goodbye"
	end

	function MyComponent:HeartbeatUpdate(dt)
	end

	function MyComponent:SteppedUpdate(dt)
	end

	function MyComponent:RenderSteppedUpdate(dt)
	end
	```
]=]
function Component.new(config: ComponentConfig)
	local customComponent = {}
	customComponent.__index = customComponent
	customComponent.__tostring = function()
		return "Component<" .. config.Tag .. ">"
	end
	customComponent[KEY_ANCESTORS] = config.Ancestors or DEFAULT_ANCESTORS
	customComponent[KEY_INST_TO_COMPONENTS] = {}
	customComponent[KEY_COMPONENTS] = {}
	customComponent[KEY_LOCK_CONSTRUCT] = {}
	customComponent[KEY_TROVE] = Trove.new()
	customComponent[KEY_EXTENSIONS] = config.Extensions or {}
	customComponent[KEY_STARTED] = false
	customComponent.Tag = config.Tag
	customComponent.Started = customComponent[KEY_TROVE]:Construct(Signal)
	customComponent.Stopped = customComponent[KEY_TROVE]:Construct(Signal)
	setmetatable(customComponent, Component)
	customComponent:_setup()
	return customComponent
end

function Component:_instantiate(instance: Instance)
	local component = setmetatable({}, self)
	component.Instance = instance
	component[KEY_ACTIVE_EXTENSIONS] = GetActiveExtensions(component, self[KEY_EXTENSIONS])
	if not ShouldConstruct(component) then
		return nil
	end
	InvokeExtensionFn(component, "Constructing")
	if type(component.Construct) == "function" then
		component:Construct()
	end
	InvokeExtensionFn(component, "Constructed")
	return component
end

function Component:_setup()
	local watchingInstances = {}

	local function StartComponent(component)
		component[KEY_STARTING] = coroutine.running()

		InvokeExtensionFn(component, "Starting")

		component:Start()
		if component[KEY_STARTING] == nil then
			-- Component's Start method stopped the component
			return
		end

		InvokeExtensionFn(component, "Started")

		local hasHeartbeatUpdate = typeof(component.HeartbeatUpdate) == "function"
		local hasSteppedUpdate = typeof(component.SteppedUpdate) == "function"
		local hasRenderSteppedUpdate = typeof(component.RenderSteppedUpdate) == "function"

		if hasHeartbeatUpdate then
			component._heartbeatUpdate = RunService.Heartbeat:Connect(function(dt)
				component:HeartbeatUpdate(dt)
			end)
		end

		if hasSteppedUpdate then
			component._steppedUpdate = RunService.Stepped:Connect(function(_, dt)
				component:SteppedUpdate(dt)
			end)
		end

		if hasRenderSteppedUpdate and not IS_SERVER then
			if component.RenderPriority then
				component._renderName = NextRenderName()
				RunService:BindToRenderStep(component._renderName, component.RenderPriority, function(dt)
					component:RenderSteppedUpdate(dt)
				end)
			else
				component._renderSteppedUpdate = RunService.RenderStepped:Connect(function(dt)
					component:RenderSteppedUpdate(dt)
				end)
			end
		end

		component[KEY_STARTED] = true
		component[KEY_STARTING] = nil

		self.Started:Fire(component)
	end

	local function StopComponent(component)
		if component[KEY_STARTING] then
			-- Stop the component during its start method invocation:
			local startThread = component[KEY_STARTING]
			if coroutine.status(startThread) ~= "normal" then
				pcall(function()
					task.cancel(startThread)
				end)
			else
				task.defer(function()
					pcall(function()
						task.cancel(startThread)
					end)
				end)
			end
			component[KEY_STARTING] = nil
		end

		if component._heartbeatUpdate then
			component._heartbeatUpdate:Disconnect()
		end

		if component._steppedUpdate then
			component._steppedUpdate:Disconnect()
		end

		if component._renderSteppedUpdate then
			component._renderSteppedUpdate:Disconnect()
		elseif component._renderName then
			RunService:UnbindFromRenderStep(component._renderName)
		end

		InvokeExtensionFn(component, "Stopping")
		component:Stop()
		InvokeExtensionFn(component, "Stopped")
		self.Stopped:Fire(component)
	end

	local function SafeConstruct(instance, id)
		if self[KEY_LOCK_CONSTRUCT][instance] ~= id then
			return nil
		end
		local component = self:_instantiate(instance)
		if self[KEY_LOCK_CONSTRUCT][instance] ~= id then
			return nil
		end
		return component
	end

	local function TryConstructComponent(instance)
		if self[KEY_INST_TO_COMPONENTS][instance] then
			return
		end
		local id = self[KEY_LOCK_CONSTRUCT][instance] or 0
		id += 1
		self[KEY_LOCK_CONSTRUCT][instance] = id
		task.defer(function()
			local component = SafeConstruct(instance, id)
			if not component then
				return
			end
			self[KEY_INST_TO_COMPONENTS][instance] = component
			table.insert(self[KEY_COMPONENTS], component)
			task.defer(function()
				if self[KEY_INST_TO_COMPONENTS][instance] == component then
					StartComponent(component)
				end
			end)
		end)
	end

	local function TryDeconstructComponent(instance)
		local component = self[KEY_INST_TO_COMPONENTS][instance]
		if not component then
			return
		end
		self[KEY_INST_TO_COMPONENTS][instance] = nil
		self[KEY_LOCK_CONSTRUCT][instance] = nil
		local components = self[KEY_COMPONENTS]
		local index = table.find(components, component)
		if index then
			local n = #components
			components[index] = components[n]
			components[n] = nil
		end
		if component[KEY_STARTED] or component[KEY_STARTING] then
			task.spawn(StopComponent, component)
		end
	end

	local function StartWatchingInstance(instance)
		if watchingInstances[instance] then
			return
		end
		local function IsInAncestorList(): boolean
			for _, parent in ipairs(self[KEY_ANCESTORS]) do
				if instance:IsDescendantOf(parent) then
					return true
				end
			end
			return false
		end
		local ancestryChangedHandle = self[KEY_TROVE]:Connect(instance.AncestryChanged, function(_, parent)
			if parent and IsInAncestorList() then
				TryConstructComponent(instance)
			else
				TryDeconstructComponent(instance)
			end
		end)
		watchingInstances[instance] = ancestryChangedHandle
		if IsInAncestorList() then
			TryConstructComponent(instance)
		end
	end

	local function InstanceTagged(instance: Instance)
		StartWatchingInstance(instance)
	end

	local function InstanceUntagged(instance: Instance)
		local watchHandle = watchingInstances[instance]
		if watchHandle then
			watchingInstances[instance] = nil
			self[KEY_TROVE]:Remove(watchHandle)
		end
		TryDeconstructComponent(instance)
	end

	self[KEY_TROVE]:Connect(CollectionService:GetInstanceAddedSignal(self.Tag), InstanceTagged)
	self[KEY_TROVE]:Connect(CollectionService:GetInstanceRemovedSignal(self.Tag), InstanceUntagged)

	local tagged = CollectionService:GetTagged(self.Tag)
	for _, instance in ipairs(tagged) do
		task.defer(InstanceTagged, instance)
	end
end

--[=[
	@tag Component Class
	@return {Component}
	Gets a table array of all existing component objects. For example,
	if there was a component class linked to the "MyComponent" tag,
	and three Roblox instances in your game had that same tag, then
	calling `GetAll` would return the three component instances.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	-- ...

	local components = MyComponent:GetAll()
	for _,component in ipairs(components) do
		component:DoSomethingHere()
	end
	```
]=]
function Component:GetAll()
	return self[KEY_COMPONENTS]
end

--[=[
	@tag Component Class
	@return Component?

	Gets an instance of a component class from the given Roblox
	instance. Returns `nil` if not found.

	```lua
	local MyComponent = require(somewhere.MyComponent)

	local myComponentInstance = MyComponent:FromInstance(workspace.SomeInstance)
	```
]=]
function Component:FromInstance(instance: Instance)
	return self[KEY_INST_TO_COMPONENTS][instance]
end

--[=[
	@tag Component Class
	@return Promise<ComponentInstance>

	Resolves a promise once the component instance is present on a given
	Roblox instance.

	An optional `timeout` can be provided to reject the promise if it
	takes more than `timeout` seconds to resolve. If no timeout is
	supplied, `timeout` defaults to 60 seconds.

	```lua
	local MyComponent = require(somewhere.MyComponent)

	MyComponent:WaitForInstance(workspace.SomeInstance):andThen(function(myComponentInstance)
		-- Do something with the component class
	end)
	```
]=]
function Component:WaitForInstance(instance: Instance, timeout: number?)
	local componentInstance = self:FromInstance(instance)
	if componentInstance and componentInstance[KEY_STARTED] then
		return Promise.resolve(componentInstance)
	end
	return Promise.fromEvent(self.Started, function(c)
		local match = c.Instance == instance
		if match then
			componentInstance = c
		end
		return match
	end)
		:andThen(function()
			return componentInstance
		end)
		:timeout(if type(timeout) == "number" then timeout else DEFAULT_TIMEOUT)
end

--[=[
	@tag Component Class
	`Construct` is called before the component is started, and should be used
	to construct the component instance.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	function MyComponent:Construct()
		self.SomeData = 32
		self.OtherStuff = "HelloWorld"
	end
	```
]=]
function Component:Construct() end

--[=[
	@tag Component Class
	`Start` is called when the component is started. At this point in time, it
	is safe to grab other components also bound to the same instance.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})
	local AnotherComponent = require(somewhere.AnotherComponent)

	function MyComponent:Start()
		-- e.g., grab another component:
		local another = self:GetComponent(AnotherComponent)
	end
	```
]=]
function Component:Start() end

--[=[
	@tag Component Class
	`Stop` is called when the component is stopped. This occurs either when the
	bound instance is removed from one of the whitelisted ancestors _or_ when
	the matching tag is removed from the instance. This also means that the
	instance _might_ be destroyed, and thus it is not safe to continue using
	the bound instance (e.g. `self.Instance`) any longer.

	This should be used to clean up the component.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	function MyComponent:Stop()
		self.SomeStuff:Destroy()
	end
	```
]=]
function Component:Stop() end

--[=[
	@tag Component Instance
	@param componentClass ComponentClass
	@return Component?

	Retrieves another component instance bound to the same
	Roblox instance.

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})
	local AnotherComponent = require(somewhere.AnotherComponent)

	function MyComponent:Start()
		local another = self:GetComponent(AnotherComponent)
	end
	```
]=]
function Component:GetComponent(componentClass)
	return componentClass[KEY_INST_TO_COMPONENTS][self.Instance]
end

--[=[
	@tag Component Class
	@function HeartbeatUpdate
	@param dt number
	@within Component

	If this method is present on a component, then it will be
	automatically connected to `RunService.Heartbeat`.

	:::note Method
	This is a method, not a function. This is a limitation
	of the documentation tool which should be fixed soon.
	:::

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	function MyComponent:HeartbeatUpdate(dt)
	end
	```
]=]
--[=[
	@tag Component Class
	@function SteppedUpdate
	@param dt number
	@within Component

	If this method is present on a component, then it will be
	automatically connected to `RunService.Stepped`.

	:::note Method
	This is a method, not a function. This is a limitation
	of the documentation tool which should be fixed soon.
	:::

	```lua
	local MyComponent = Component.new({Tag = "MyComponent"})

	function MyComponent:SteppedUpdate(dt)
	end
	```
]=]
--[=[
	@tag Component Class
	@function RenderSteppedUpdate
	@param dt number
	@within Component
	@client

	If this method is present on a component, then it will be
	automatically connected to `RunService.RenderStepped`. If
	the `[Component].RenderPriority` field is found, then the
	component will instead use `RunService:BindToRenderStep()`
	to bind the function.

	:::note Method
	This is a method, not a function. This is a limitation
	of the documentation tool which should be fixed soon.
	:::

	```lua
	-- Example that uses `RunService.RenderStepped` automatically:

	local MyComponent = Component.new({Tag = "MyComponent"})

	function MyComponent:RenderSteppedUpdate(dt)
	end
	```
	```lua
	-- Example that uses `RunService:BindToRenderStep` automatically:

	local MyComponent = Component.new({Tag = "MyComponent"})

	-- Defining a RenderPriority will force the component to use BindToRenderStep instead
	MyComponent.RenderPriority = Enum.RenderPriority.Camera.Value

	function MyComponent:RenderSteppedUpdate(dt)
	end
	```
]=]

function Component:Destroy()
	self[KEY_TROVE]:Destroy()
end

return Component


local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

return function(ctx: Test.TestContext)
	local Component = require(script.Parent)

	local TAG = "__KnitTestComponent__"

	local taggedInstanceFolder

	local function CreateTaggedInstance()
		local folder = Instance.new("Folder")
		CollectionService:AddTag(folder, TAG)
		folder.Name = "ComponentTest"
		folder.Archivable = false
		folder.Parent = taggedInstanceFolder
		return folder
	end

	local ExtensionTest = {}
	function ExtensionTest.ShouldConstruct(_component)
		return true
	end
	function ExtensionTest.Constructing(component)
		component.Data = "a"
		component.DidHeartbeat = false
		component.DidStepped = false
		component.DidRenderStepped = false
	end
	function ExtensionTest.Constructed(component)
		component.Data ..= "c"
	end
	function ExtensionTest.Starting(component)
		component.Data ..= "d"
	end
	function ExtensionTest.Started(component)
		component.Data ..= "f"
	end
	function ExtensionTest.Stopping(component)
		component.Data ..= "g"
	end
	function ExtensionTest.Stopped(component)
		component.Data ..= "i"
	end

	local TestComponentMain = Component.new({
		Tag = TAG,
		Ancestors = { workspace, game:GetService("Lighting") },
		Extensions = { ExtensionTest },
	})

	local AnotherComponent = Component.new({ Tag = TAG })
	function AnotherComponent:GetData()
		return true
	end

	function TestComponentMain:Construct()
		self.Data ..= "b"
	end

	function TestComponentMain:Start()
		self.Another = self:GetComponent(AnotherComponent)
		self.Data ..= "e"
	end

	function TestComponentMain:Stop()
		self.Data ..= "h"
	end

	function TestComponentMain:HeartbeatUpdate(_dt)
		self.DidHeartbeat = true
	end

	function TestComponentMain:SteppedUpdate(_dt)
		self.DidStepped = true
	end

	function TestComponentMain:RenderSteppedUpdate(_dt)
		self.DidRenderStepped = true
	end

	ctx:BeforeAll(function()
		taggedInstanceFolder = Instance.new("Folder")
		taggedInstanceFolder.Name = "KnitComponentTest"
		taggedInstanceFolder.Archivable = false
		taggedInstanceFolder.Parent = workspace
	end)

	ctx:AfterAll(function()
		taggedInstanceFolder:Destroy()
		TestComponentMain:Destroy()
	end)

	ctx:Describe("Component", function()
		ctx:AfterEach(function()
			taggedInstanceFolder:ClearAllChildren()
		end)

		ctx:Test("should capture start and stop events", function()
			local didStart = 0
			local didStop = 0
			local started = TestComponentMain.Started:Connect(function()
				didStart += 1
			end)
			local stopped = TestComponentMain.Stopped:Connect(function()
				didStop += 1
			end)
			local instance = CreateTaggedInstance()
			task.wait()
			instance:Destroy()
			task.wait()
			started:Disconnect()
			stopped:Disconnect()
			ctx:Expect(didStart):ToBe(1)
			ctx:Expect(didStop):ToBe(1)
		end)

		ctx:Test("should be able to get component from the instance", function()
			local instance = CreateTaggedInstance()
			task.wait()
			local component = TestComponentMain:FromInstance(instance)
			ctx:Expect(component):ToBeOk()
		end)

		ctx:Test("should be able to get all component instances existing", function()
			local numComponents = 3
			local instances = table.create(numComponents)
			for i = 1, numComponents do
				local instance = CreateTaggedInstance()
				instances[i] = instance
			end
			task.wait()
			local components = TestComponentMain:GetAll()
			ctx:Expect(components):ToBeA("table")
			ctx:Expect(components):ToHaveLength(numComponents)
			for _, c in components do
				ctx:Expect(table.find(instances, c.Instance)):ToBeOk()
			end
		end)

		ctx:Test("should call lifecycle methods and extension functions", function()
			local instance = CreateTaggedInstance()
			task.wait(0.2)
			local component = TestComponentMain:FromInstance(instance)
			ctx:Expect(component):ToBeOk()
			ctx:Expect(component.Data):ToBe("abcdef")
			ctx:Expect(component.DidHeartbeat):ToBe(true)
			ctx:Expect(component.DidStepped):ToBe(RunService:IsRunning())
			ctx:Expect(component.DidRenderStepped):Not():ToBe(true)
			instance:Destroy()
			task.wait()
			ctx:Expect(component.Data):ToBe("abcdefghi")
		end)

		ctx:Test("should get another component linked to the same instance", function()
			local instance = CreateTaggedInstance()
			task.wait()
			local component = TestComponentMain:FromInstance(instance)
			ctx:Expect(component):ToBeOk()
			ctx:Expect(component.Another):ToBeOk()
			ctx:Expect(component.Another:GetData()):ToBe(true)
		end)

		ctx:Test("should use extension to decide whether or not to construct", function()
			local e1 = { c = true }
			function e1.ShouldConstruct(_component)
				return e1.c
			end

			local e2 = { c = true }
			function e2.ShouldConstruct(_component)
				return e2.c
			end

			local e3 = { c = true }
			function e3.ShouldConstruct(_component)
				return e3.c
			end

			local c1 = Component.new({ Tag = TAG, Extensions = { e1 } })
			local c2 = Component.new({ Tag = TAG, Extensions = { e1, e2 } })
			local c3 = Component.new({ Tag = TAG, Extensions = { e1, e2, e3 } })

			local function SetE(a, b, c)
				e1.c = a
				e2.c = b
				e3.c = c
			end

			local function Check(inst, comp, shouldExist)
				local c = comp:FromInstance(inst)
				if shouldExist then
					ctx:Expect(c):ToBeOk()
				else
					ctx:Expect(c):ToBeNil()
				end
			end

			local function CreateAndCheckAll(a, b, c)
				local instance = CreateTaggedInstance()
				task.wait()
				Check(instance, c1, a)
				Check(instance, c2, b)
				Check(instance, c3, c)
			end

			-- All green:
			SetE(true, true, true)
			CreateAndCheckAll(true, true, true)

			-- All red:
			SetE(false, false, false)
			CreateAndCheckAll(false, false, false)

			-- One red:
			SetE(true, false, true)
			CreateAndCheckAll(true, false, false)

			-- One green:
			SetE(false, false, true)
			CreateAndCheckAll(false, false, false)
		end)

		ctx:Test("should decide whether or not to use extend", function()
			local e1 = { extend = true }
			function e1.ShouldExtend(_component)
				return e1.extend
			end
			function e1.Constructing(component)
				component.E1 = true
			end

			local e2 = { extend = true }
			function e2.ShouldExtend(_component)
				return e2.extend
			end
			function e2.Constructing(component)
				component.E2 = true
			end

			local TestComponent = Component.new({ Tag = TAG, Extensions = { e1, e2 } })

			local function SetAndCheck(ex1, ex2)
				e1.extend = ex1
				e2.extend = ex2
				local instance = CreateTaggedInstance()
				task.wait()
				local component = TestComponent:FromInstance(instance)
				ctx:Expect(component):ToBeOk()
				if ex1 then
					ctx:Expect(component.E1):ToBe(true)
				else
					ctx:Expect(component.E1):ToBeNil()
				end
				if ex2 then
					ctx:Expect(component.E2):ToBe(true)
				else
					ctx:Expect(component.E2):ToBeNil()
				end
			end

			SetAndCheck(true, true)
			SetAndCheck(false, false)
			SetAndCheck(true, false)
			SetAndCheck(false, true)
		end)

		ctx:Test("should allow yielding within construct", function()
			local CUSTOM_TAG = "CustomTag"

			local TestComponent = Component.new({ Tag = CUSTOM_TAG })

			local numConstruct = 0

			function TestComponent:Construct()
				numConstruct += 1
				task.wait(0.5)
			end

			local p = Instance.new("Part")
			p.Anchored = true
			p.Parent = game:GetService("ReplicatedStorage")
			CollectionService:AddTag(p, CUSTOM_TAG)
			local newP = p:Clone()
			newP.Parent = workspace

			task.wait(0.6)

			ctx:Expect(numConstruct):ToBe(1)
			p:Destroy()
			newP:Destroy()
		end)

		ctx:Test("should wait for instance", function()
			local p = Instance.new("Part")
			p.Anchored = true
			p.Parent = workspace
			task.delay(0.1, function()
				CollectionService:AddTag(p, TAG)
			end)
			local success, c = TestComponentMain:WaitForInstance(p):timeout(1):await()
			ctx:Expect(success):ToBe(true)
			ctx:Expect(c):ToBeA("table")
			ctx:Expect(c.Instance):ToBe(p)
			p:Destroy()
		end)
	end)
end


--!strict

-- Concur
-- Stephen Leitnick
-- May 01, 2022

type Error = any
type AnyFn = (...any) -> ...any

--[=[
	@class Concur

	Concurrency class for helping run tasks concurrently. In other words, Concur allows
	developers to watch coroutines/threads. Completion status, returned values, and
	errors can all be tracked.

	For instance, Concur could be used to concurrently save all player data
	at the same time when the game closes down:

	```lua
	game:BindToClose(function()
		local all = {}
		for _,player in Players:GetPlayers() do
			local save = Concur.spawn(function()
				DoSomethingToSaveData(player)
			end)
			table.insert(all, save)
		end
		local allConcur = Concur.all(all)
		allConcur:Await()
	end)
	```
]=]
local Concur = {}
Concur.__index = Concur

--[=[
	@within Concur
	@interface Errors
	.Stopped "Stopped"
	.Timeout "Timeout"
]=]

--[=[
	@within Concur
	@readonly
	@prop Errors Errors
]=]
Concur.Errors = {
	Stopped = "Stopped",
	Timeout = "Timeout",
}

function Concur._new(fn: AnyFn, spawner: AnyFn, ...: any): Concur
	local self: Concur = setmetatable({
		_completed = false,
		_res = nil,
		_err = nil,
		_awaitingThreads = {},
		_thread = nil,
	}, Concur)

	self._thread = spawner(function(...)
		local pcallRes = table.pack(pcall(fn, ...))
		self._completed = true
		self._err = if not pcallRes[1] then pcallRes[2] else nil
		if self._err ~= nil then
			for _, thread in ipairs(self._awaitingThreads) do
				task.spawn(thread, self._err)
			end
		else
			local res = table.move(pcallRes, 2, #pcallRes, 1, table.create(#pcallRes - 1))
			self._res = res
			for _, thread in ipairs(self._awaitingThreads) do
				task.spawn(thread, nil, table.unpack(res, 1, res.n))
			end
		end
	end, ...)

	return self
end

--[=[
	Spawns the function using `task.spawn`.

	```lua
	local c = Concur.spawn(function()
		task.wait(5)
		return "Hello!"
	end)

	c:OnCompleted(function(err, msg)
		if err then
			error(err)
		end
		print(msg) --> Hello!
	end))
	```
]=]
function Concur.spawn(fn: AnyFn, ...: any): Concur
	if type(fn) ~= "function" then
		error("Concur.spawn argument must be a function; got " .. type(fn), 2)
	end
	return Concur._new(fn, task.spawn, ...)
end

--[=[
	Same as `Concur.spawn`, but uses `task.defer` internally.
]=]
function Concur.defer(fn: AnyFn, ...: any): Concur
	if type(fn) ~= "function" then
		error("Concur.defer argument must be a function; got " .. type(fn), 2)
	end
	return Concur._new(fn, task.defer, ...)
end

--[=[
	Same as `Concur.spawn`, but uses `task.delay` internally.
]=]
function Concur.delay(delayTime: number, fn: AnyFn, ...: any): Concur
	if type(fn) ~= "function" then
		error("Concur.delay argument must be a function; got " .. type(fn), 2)
	end
	return Concur._new(fn, function(...)
		return task.delay(delayTime, ...)
	end, ...)
end

--[=[
	Resolves to the given value right away.

	```lua
	local val = Concur.value(10)
	val:OnCompleted(function(v)
		print(v) --> 10
	end)
	```
]=]
function Concur.value(value: any): Concur
	return Concur.spawn(function()
		return value
	end)
end

--[=[
	Completes the Concur instance once the event is fired and the predicate
	function returns `true` (if no predicate is given, then completes once
	the event first fires).

	The Concur instance will return the values given by the event.

	```lua
	-- Wait for next player to touch an object:
	local touch = Concur.event(part.Touched, function(toucher)
		return Players:GetPlayerFromCharacter(toucher.Parent) ~= nil
	end)

	touch:OnCompleted(function(err, toucher)
		print(toucher)
	end)
	```
]=]
function Concur.event(event: RBXScriptSignal, predicate: ((...any) -> boolean)?)
	local connection, thread

	connection = event:Connect(function(...)
		if not thread then
			return
		end
		if predicate == nil or predicate(...) then
			connection:Disconnect()
			task.spawn(thread, ...)
		end
	end)

	local c = Concur.spawn(function()
		thread = coroutine.running()
		return coroutine.yield()
	end)

	c:OnCompleted(function(err)
		connection:Disconnect()
		if coroutine.status(thread) == "suspended" then
			task.spawn(thread, err)
		end
	end)

	return c
end

--[=[
	Completes once _all_ Concur instances have been completed. All values
	will be available in a packed table in the same order they were passed.

	```lua
	local c1 = Concur.spawn(function()
		return 10
	end)

	local c2 = Concur.delay(0.5, function()
		return 15
	end)

	local c3 = Concur.value(20)

	local c4 = Concur.spawn(function()
		error("failed")
	end)

	Concur.all({c1, c2, c3}):OnCompleted(function(err, values)
		print(values) --> {{nil, 10}, {nil, 15}, {nil, 20}, {"failed", nil}}
	end)
	```
]=]
function Concur.all(concurs: { Concur }): Concur
	if #concurs == 0 then
		return Concur.value(nil)
	end

	return Concur.spawn(function()
		local numCompleted = 0
		local total = #concurs
		local thread = coroutine.running()
		local allRes = table.create(total)
		for i, concur in ipairs(concurs) do
			concur:OnCompleted(function(...)
				allRes[i] = table.pack(...)
				numCompleted += 1
				if numCompleted >= total and coroutine.status(thread) == "suspended" then
					task.spawn(thread)
				end
			end)
		end
		if numCompleted < total then
			coroutine.yield()
		end
		return allRes
	end)
end

--[=[
	Completes once the first Concur instance is completed _without an error_. All other Concur
	instances are then stopped.

	```lua
	local c1 = Concur.delay(1, function()
		return 10
	end)

	local c2 = Concur.delay(0.5, function()
		return 5
	end)

	Concur.first({c1, c2}):OnCompleted(function(err, num)
		print(num) --> 5
	end)
	```
]=]
function Concur.first(concurs: { Concur }): Concur
	if #concurs == 0 then
		return Concur.value(nil)
	end

	return Concur.spawn(function()
		local thread = coroutine.running()
		local res = nil
		local firstConcur = nil
		for _, concur in ipairs(concurs) do
			concur:OnCompleted(function(err, ...)
				if res or err ~= nil then
					return
				end
				firstConcur = concur
				res = table.pack(...)
				if coroutine.status(thread) == "suspended" then
					task.spawn(thread)
				end
			end)
		end
		if res == nil then
			coroutine.yield()
		end
		for _, concur in ipairs(concurs) do
			if concur == firstConcur then
				continue
			end
			concur:Stop()
		end
		return table.unpack(res, 1, res.n)
	end)
end

--[=[
	Stops the Concur instance. The underlying thread will be cancelled using
	`task.cancel`. Any bound `OnCompleted` functions or threads waiting with
	`Await` will be completed with the error `Concur.Errors.Stopped`.

	```lua
	local c = Concur.spawn(function()
		for i = 1,10 do
			print(i)
			task.wait(1)
		end
	end)

	task.wait(2.5)
	c:Stop() -- At this point, will have only printed 1 and 2
	```
]=]
function Concur:Stop()
	if self._completed then
		return
	end
	self._completed = true
	self._err = Concur.Errors.Stopped
	task.cancel(self._thread)
	for _, thread: thread in ipairs(self._awaitingThreads) do
		task.spawn(thread, Concur.Errors.Stopped)
	end
end

--[=[
	Check if the Concur instance is finished.
]=]
function Concur:IsCompleted(): boolean
	return self._completed
end

--[=[
	@yields
	Yields the calling thread until the Concur instance is completed:

	```lua
	local c = Concur.delay(5, function()
		return "Hi"
	end)

	local err, msg = c:Await()
	print(msg) --> Hi
	```

	The `Await` method can be called _after_ the Concur instance
	has been completed too, in which case the completed values
	will be returned immediately without yielding the thread:

	```lua
	local c = Concur.spawn(function()
		return 10
	end)

	task.wait(5)
	-- Called after 'c' has been completed, but still captures the value:
	local err, num = c:Await()
	print(num) --> 10
	```

	It is always good practice to make sure that the `err` value is handled
	by checking if it is not nil:

	```lua
	local c = Concur.spawn(function()
		error("failed")
	end)

	local err, value = c:Await()

	if err ~= nil then
		print(err) --> failed
		-- Handle error `err`
	else
		-- Handle `value`
	end
	```

	This will stop awaiting if the Concur instance was stopped
	too, in which case the `err` will be equal to
	`Concur.Errors.Stopped`:

	```lua
	local c = Concur.delay(10, function() end)
	c:Stop()
	local err = c:Await()
	if err == Concur.Errors.Stopped then
		print("Was stopped")
	end
	```

	An optional timeout can be given, which will return the
	`Concur.Errors.Timeout` error if timed out. Timing out
	does _not_ stop the Concur instance, so other callers
	to `Await` or `OnCompleted` can still grab the resulting
	values.

	```lua
	local c = Concur.delay(10, function() end)
	local err = c:Await(1)
	if err == Concur.Errors.Timeout then
		-- Handle timeout
	end
	```
]=]
function Concur:Await(timeout: number?): (Error, ...any?)
	if self._completed then
		if self._err ~= nil then
			return self._err
		else
			return nil, if self._res == nil then nil else table.unpack(self._res, 1, self._res.n)
		end
	end

	local thread = coroutine.running()
	table.insert(self._awaitingThreads, thread)

	if timeout then
		local delayThread = task.delay(timeout, function()
			local index = table.find(self._awaitingThreads, thread)
			if index then
				table.remove(self._awaitingThreads, index)
				task.spawn(thread, Concur.Errors.Timeout)
			end
		end)
		local res = table.pack(coroutine.yield())
		if coroutine.status(delayThread) ~= "normal" then
			task.cancel(delayThread)
		end
		return table.unpack(res, 1, res.n)
	else
		return coroutine.yield()
	end
end

--[=[
	Calls the given function once the Concur instance is completed:

	```lua
	local c = Concur.delay(5, function()
		return "Hi"
	end)

	c:OnCompleted(function(err, msg)
		print(msg) --> Hi
	end)
	```

	A function is returned that can be used to unbind the function to
	no longer fire when the Concur instance is completed:

	```lua
	local c = Concur.delay(5, function() end)
	local unbind = c:OnCompleted(function()
		print("Completed")
	end)
	unbind()
	-- Never prints "Completed"
	```

	The `OnCompleted` method can be called _after_ the Concur instance
	has been completed too, in which case the given function will be
	called immediately with the completed values:

	```lua
	local c = Concur.spawn(function()
		return 10
	end)

	task.wait(5)
	-- Called after 'c' has been completed, but still captures the value:
	c:OnCompleted(function(err, num)
		print(num) --> 10
	end)
	```

	It is always good practice to make sure that the `err` value is handled
	by checking if it is not nil:

	```lua
	local c = Concur.spawn(function()
		error("failed")
	end)

	c:OnCompleted(function(err, value)
		if err ~= nil then
			print(err) --> failed
			-- Handle error `err`
			return
		end
		-- Handle `value`
	end)
	```

	This will call the function if the Concur instance was stopped
	too, in which case the `err` will be equal to
	`Concur.Errors.Stopped`:

	```lua
	local c = Concur.delay(10, function() end)
	c:OnCompleted(function(err)
		if err == Concur.Errors.Stopped then
			print("Was stopped")
		end
	end)
	c:Stop()
	```

	An optional timeout can also be supplied, which will call the
	function with the `Concur.Errors.Timeout` error:

	```lua
	local c = Concur.delay(10, function() end)
	c:OnCompleted(function(err)
		if err == Concur.Errors.Timeout then
			-- Handle timeout
		end
	end, 1)
	```
]=]
function Concur:OnCompleted(fn: (Error, ...any?) -> (), timeout: number?): () -> ()
	local thread = task.spawn(function()
		fn(self:Await(timeout))
	end)

	-- Unbind:
	return function()
		task.cancel(thread)
		local index = table.find(self._awaitingThreads, thread)
		if index then
			table.remove(self._awaitingThreads, index)
		end
	end
end

type ConcurObj = {
	_completed: boolean,
	_res: { any }?,
	_err: string?,
	_awaitingThreads: { thread },
	_thread: thread?,
}

export type Concur = typeof(setmetatable({} :: ConcurObj, Concur))

return Concur