export type Sequent<T> = {
	Fire: (self: Sequent<T>, T) -> (),
	Connect: (self: Sequent<T>, callback: (SequentEvent<T>) -> ()) -> SequentConnection,
	Once: (self: Sequent<T>, callback: (SequentEvent<T>) -> ()) -> SequentConnection,
	Cancel: (self: Sequent<T>) -> (),
	Destroy: (self: Sequent<T>) -> (),
}

--[=[
	@interface SequentConnection
	@within Sequent
	.Connected boolean
	.Disconnect (self: SequentConnection) -> ()

	```lua
	print(sequent.Connected)
	sequent:Disconnect()
	```
]=]
export type SequentConnection = {
	Connected: boolean,
	Disconnect: (self: SequentConnection) -> (),
}

--[=[
	@interface SequentEvent<T>
	@within Sequent
	.Value T
	.Cancellable boolean
	.Cancel (self: SequentEvent<T>) -> ()

	Events are passed to connected callbacks when sequents are fired. Events
	can be cancelled as well, which prevents the event from propagating to
	other connected callbacks during the same firing. This can be used to
	sink events if desired.

	```lua
	sequent:Connect(function(event)
		print(event.Value)
		event:Cancel()
	end, 0)
	```
]=]
export type SequentEvent<T> = {
	Value: T,
	Cancellable: boolean,
	Cancel: (self: SequentEvent<T>) -> (),
}

type InternalSequentConnection = SequentConnection & {
	_priority: number,
	_sequent: Sequent<unknown>,
}

-----------------------------------------------------------------------------
-- Connection
local SequentConnection = {}
SequentConnection.__index = SequentConnection

function SequentConnection:Disconnect()
	self._sequent:_disconnect(self)
end

-----------------------------------------------------------------------------
-- Sequent

--[=[
	@interface Priority
	@within Sequent
	.Highest math.huge
	.High 1000
	.Normal 0
	.Low -1000
	.Lowest -math.huge

	```lua
	sequent:Connect(fn, Sequent.Priority.Highest)
	```
]=]
local Priority = {
	Highest = math.huge,
	High = 1000,
	Normal = 0,
	Low = -1000,
	Lowest = -math.huge,
}

--[=[
	@class Sequent

	Sequent is a signal-like structure that executes connections in a serial nature. Each
	connection must fully complete before the next is run. Connections can be prioritized
	and cancelled.

	```lua
	local sequent = Sequent.new()

	sequent:Connect(
		function(event)
			print("Got value", event.Value)
			event:Cancel()
		end,
		Sequent.Priority.Highest,
	)

	sequent:Connect(
		function(event)
			print("This won't print!")
		end,
		Sequent.Priority.Lowest,
	)

	sequent:Fire("Test")
	```
]=]
local Sequent = {}
Sequent.__index = Sequent

--[=[
	Constructs a new Sequent. If `cancellable` is `true`, then
	connected handlers can cancel event propagation.
]=]
function Sequent.new<T>(cancellable: boolean?): Sequent<T>
	local self = setmetatable({
		_connections = {},
		_firing = false,
		_queuedDisconnect = false,

		_firingThread = nil,
		_taskThread = nil,

		_cancellable = not not cancellable,
	}, Sequent)

	return self
end

--[=[
	@yields
	Fires the Sequent with the given value.

	This method will yield until all connections complete. Errors will
	bubble up if they occur within a connection.
]=]
function Sequent:Fire<T>(value: T)
	assert(not self._firing, "cannot fire while already firing")
	self._firing = true

	local cancelled = false
	local event: SequentEvent<T> = table.freeze({
		Value = value,
		Cancellable = self._cancellable,
		Cancel = function(_evt)
			if not self._cancellable then
				warn("attempted to cancel non-cancellable event")
				return
			end
			cancelled = true
		end,
	})

	local thread = coroutine.running()
	self._firingThread = thread
	for _, connection in self._connections do
		if not connection.Connected then
			continue
		end

		-- Run the task:
		local success, err = nil, nil
		local taskThread = task.spawn(function()
			self._taskThread = coroutine.running()

			local s, e = pcall(function()
				connection._callback(event)
			end)

			self._taskThread = nil

			-- Resume the parent thread if it yielded:
			if coroutine.status(thread) == "suspended" then
				task.spawn(thread, s, e)
			else
				success, err = s, e
			end
		end)

		-- If the task thread yielded, yield this thread too:
		if success == nil then
			success, err = coroutine.yield()
		end

		self._firingThread = nil

		-- Throw error if the task failed:
		if not success then
			error(debug.traceback(taskThread, tostring(err)))
		end

		if cancelled then
			break
		end
	end

	-- If connections were disconnected while firing connections, disconnect them now:
	if self._queuedDisconnect then
		self._queuedDisconnect = false
		for i = #self._connections, 1, -1 do
			local connection = self._connections[i]
			if not connection.Connected then
				self:_removeConnection(connection)
			end
		end
	end

	self._firing = false
end

--[=[
	Returns `true` if the Sequent is currently firing.
]=]
function Sequent:IsFiring(): boolean
	return self._firing
end

--[=[
	Connects a callback to the Sequent, which gets called anytime `Fire`
	is called.

	The given `priority` indicates the firing priority of the callback. Higher
	priority values will be run first. There are a few defaults available via
	`Sequent.Priority`.
]=]
function Sequent:Connect(callback: (...unknown) -> (), priority: number): SequentConnection
	assert(not self._firing, "cannot connect while firing")

	local connection = setmetatable({
		Connected = true,
		_callback = callback,
		_priority = priority,
		_sequent = self,
	}, SequentConnection)

	-- Find the correct index to remain sorted by priority:
	local idx = #self._connections + 1
	for i, c: InternalSequentConnection in self._connections do
		if c._priority < priority then
			idx = i
			break
		end
	end

	table.insert(self._connections, idx, connection)

	return connection
end

--[=[
	`Once()` is the same as `Connect()`, except the connection is automatically
	disconnected after being fired once.
]=]
function Sequent:Once(callback: (...unknown) -> (), priority: number): SequentConnection
	local connection: SequentConnection

	connection = self:Connect(function(...)
		if not connection.Connected then
			return
		end
		connection:Disconnect()
		callback(...)
	end, priority)

	return connection
end

--[=[
	Cancels a currently-firing Sequent.
]=]
function Sequent:Cancel()
	if not self._firing then
		return
	end

	if self._taskThread == coroutine.running() then
		error("cannot cancel sequent from connected task", 2)
	end

	if self._taskThread then
		-- pcall needed as the task thread may have yielded from a call to
		-- a roblox service, which will throw an error when calling task.cancel:
		pcall(function()
			task.cancel(self._taskThread)
		end)
		self._taskThread = nil
	end

	if self._firingThread then
		local thread = self._firingThread
		self._firingThread = nil
		task.spawn(thread, true)
	end
end

--[=[
	Cleans up the Sequent. All connections are disconnected. The Sequent is cancelled
	if it is currently firing.
]=]
function Sequent:Destroy()
	self:Cancel()

	for _, connection in self._connections do
		connection.Connected = false
	end
end

function Sequent:_disconnect(connection)
	if not connection.Connected then
		return
	end
	connection.Connected = false

	if self._firing then
		self._queuedDisconnect = true
		return
	end

	self:_removeConnection(connection)
end

function Sequent:_removeConnection(connection)
	local idx = table.find(self._connections, connection)
	if idx then
		table.remove(self._connections, idx)
	end
end

return table.freeze({
	new = Sequent.new,
	Priority = Priority,
})


--!strict

-- Ser
-- Stephen Leitnick
-- August 28, 2020

--[[

	Ser is a serialization/deserialization utility module that is used
	by Knit to automatically serialize/deserialize values passing
	through remote functions and remote events.

	Ser.Classes = {
		[ClassName] = {
			Serialize = (value) -> serializedValue
			Deserialize = (value) => deserializedValue
		}
	}

	Ser.SerializeArgs(...)            -> table
	Ser.SerializeArgsAndUnpack(...)   -> Tuple
	Ser.DeserializeArgs(...)          -> table
	Ser.DeserializeArgsAndUnpack(...) -> Tuple
	Ser.Serialize(value: any)         -> any
	Ser.Deserialize(value: any)       -> any
	Ser.UnpackArgs(args: table)       -> Tuple

--]]

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

local Option = require(script.Parent.Option)

--[=[
	@class Ser

	Library for serializing and deserializing data.

	See the `Classes` property for information on extending the use
	of the Ser library to include other classes.

]=]
local Ser = {}

--[=[
	@within Ser
	@prop Classes table

	A dictionary of classes along with a Serialize and Deserialize function.
	For instance, the default class added is the Option class, which looks
	like the following:

	```lua
	Ser.Classes.Option = {
		Serialize = function(opt) return opt:Serialize() end;
		Deserialize = Option.Deserialize;
	}
	```

	Add to this table in order to extend what classes are automatically
	serialized/deserialized.

	The Ser library checks every object's `ClassName` field in both serialized
	and deserialized data in order to map it to the correct function within
	the Classes table.
]=]
Ser.Classes = {
	Option = {
		Serialize = function(opt)
			return opt:Serialize()
		end,
		Deserialize = Option.Deserialize,
	},
}

--[=[
	@param ... any
	@return args: table
	Serializes the arguments and returns the serialized values in a table.
]=]
function Ser.SerializeArgs(...: any): Args
	local args = table.pack(...)
	for i, arg in ipairs(args) do
		if type(arg) == "table" then
			local ser = Ser.Classes[arg.ClassName]
			if ser then
				args[i] = ser.Serialize(arg)
			end
		end
	end
	return args
end

--[=[
	@param ... any
	@return args: ...any
	Serializes the arguments and returns the serialized values.
]=]
function Ser.SerializeArgsAndUnpack(...: any): ...any
	local args = Ser.SerializeArgs(...)
	return table.unpack(args, 1, args.n)
end

--[=[
	@param ... any
	@return args: table
	Deserializes the arguments and returns the deserialized values in a table.
]=]
function Ser.DeserializeArgs(...: any): Args
	local args = table.pack(...)
	for i, arg in ipairs(args) do
		if type(arg) == "table" then
			local ser = Ser.Classes[arg.ClassName]
			if ser then
				args[i] = ser.Deserialize(arg)
			end
		end
	end
	return args
end

--[=[
	@param ... any
	@return args: table
	Deserializes the arguments and returns the deserialized values.
]=]
function Ser.DeserializeArgsAndUnpack(...: any): ...any
	local args = Ser.DeserializeArgs(...)
	return table.unpack(args, 1, args.n)
end

--[=[
	@param value any
	@return any
	Serializes the given value.
]=]
function Ser.Serialize(value: any): any
	if type(value) == "table" then
		local ser = Ser.Classes[value.ClassName]
		if ser then
			value = ser.Serialize(value)
		end
	end
	return value
end

--[=[
	@param value any
	@return any
	Deserializes the given value.
]=]
function Ser.Deserialize(value: any): any
	if type(value) == "table" then
		local ser = Ser.Classes[value.ClassName]
		if ser then
			value = ser.Deserialize(value)
		end
	end
	return value
end

--[=[
	@param value any
	@return any
	Unpacks the arguments returned by either `SerializeArgs` or `DeserializeArgs`.
]=]
function Ser.UnpackArgs(value: Args): ...any
	return table.unpack(value, 1, value.n)
end

return Ser


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

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

	ctx:Describe("SerializeArgs", function()
		ctx:Test("should serialize an option", function()
			local opt = Option.Some(32)
			local serOpt = table.unpack(Ser.SerializeArgs(opt))
			ctx:Expect(serOpt.ClassName):ToBe("Option")
			ctx:Expect(serOpt.Value):ToBe(32)
		end)
	end)

	ctx:Describe("SerializeArgsAndUnpack", function()
		ctx:Test("should serialize an option", function()
			local opt = Option.Some(32)
			local serOpt = Ser.SerializeArgsAndUnpack(opt)
			ctx:Expect(serOpt.ClassName):ToBe("Option")
			ctx:Expect(serOpt.Value):ToBe(32)
		end)
	end)

	ctx:Describe("DeserializeArgs", function()
		ctx:Test("should deserialize args to option", function()
			local serOpt = {
				ClassName = "Option",
				Value = 32,
			}
			local opt = table.unpack(Ser.DeserializeArgs(serOpt))
			ctx:Expect(Option.Is(opt)):ToBe(true)
			ctx:Expect(opt:Contains(32)):ToBe(true)
		end)
	end)

	ctx:Describe("DeserializeArgsAndUnpack", function()
		ctx:Test("should deserialize args to option", function()
			local serOpt = {
				ClassName = "Option",
				Value = 32,
			}
			local opt = Ser.DeserializeArgsAndUnpack(serOpt)
			ctx:Expect(Option.Is(opt)):ToBe(true)
			ctx:Expect(opt:Contains(32)):ToBe(true)
		end)
	end)
end


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

local Test = require(ServerScriptService.TestRunner.Test)

local function AwaitStop(shake): number
	local start = os.clock()
	shake:Update()
	while shake:IsShaking() do
		task.wait()
		shake:Update()
	end
	return os.clock() - start
end

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

	ctx:Describe("Construct", function()
		ctx:Test("should construct a new shake instance", function()
			ctx:Expect(function()
				local _shake = Shake.new()
			end)
				:Not()
				:ToThrow()
		end)
	end)

	ctx:Describe("Static Functions", function()
		ctx:Test("should get next render name", function()
			local r1 = Shake.NextRenderName()
			local r2 = Shake.NextRenderName()
			local r3 = Shake.NextRenderName()
			ctx:Expect(r1):ToBeA("string")
			ctx:Expect(r2):ToBeA("string")
			ctx:Expect(r3):ToBeA("string")
			ctx:Expect(r1):Not():ToBe(r2)
			ctx:Expect(r2):Not():ToBe(r3)
			ctx:Expect(r3):Not():ToBe(r1)
		end)

		ctx:Test("should perform inverse square", function()
			local vector = Vector3.new(10, 10, 10)
			local distance = 10
			local expectedIntensity = 1 / (distance * distance)
			local expectedVector = vector * expectedIntensity
			local vectorInverseSq = Shake.InverseSquare(vector, distance)
			ctx:Expect(typeof(vectorInverseSq)):ToBe("Vector3")
			ctx:Expect(vectorInverseSq):ToBe(expectedVector)
		end)
	end)

	ctx:Describe("Cloning", function()
		ctx:Test("should clone a shake instance", function()
			local shake1 = Shake.new()
			shake1.Amplitude = 5
			shake1.Frequency = 2
			shake1.FadeInTime = 3
			shake1.FadeOutTime = 4
			shake1.SustainTime = 6
			shake1.Sustain = true
			shake1.PositionInfluence = Vector3.new(1, 2, 3)
			shake1.RotationInfluence = Vector3.new(3, 2, 1)
			shake1.TimeFunction = function()
				return os.clock()
			end
			local shake2 = shake1:Clone()
			ctx:Expect(shake2):ToBeA("table")
			ctx:Expect(getmetatable(shake2)):ToBe(getmetatable(shake1))
			ctx:Expect(shake2):Not():ToBe(shake1)
			local clonedFields = {
				"Amplitude",
				"Frequency",
				"FadeInTime",
				"FadeOutTime",
				"SustainTime",
				"Sustain",
				"PositionInfluence",
				"RotationInfluence",
			}
			for _, field in clonedFields do
				ctx:Expect(shake1[field]):ToBe(shake2[field])
			end
			ctx:Expect(shake1.TimeFunction == shake2.TimeFunction):ToBe(true)
		end)

		ctx:Test("should clone a shake instance but ignore running state", function()
			local shake1 = Shake.new()
			shake1:Start()
			local shake2 = shake1:Clone()
			ctx:Expect(shake1:IsShaking()):ToBe(true)
			ctx:Expect(shake2:IsShaking()):ToBe(false)
		end)
	end)

	ctx:Describe("Shaking", function()
		ctx:Test("should start", function()
			local shake = Shake.new()
			ctx:Expect(shake:IsShaking()):ToBe(false)
			shake:Start()
			ctx:Expect(shake:IsShaking()):ToBe(true)
		end)

		ctx:Test("should stop", function()
			local shake = Shake.new()
			shake:Start()
			ctx:Expect(shake:IsShaking()):ToBe(true)
			shake:Stop()
			ctx:Expect(shake:IsShaking()):ToBe(false)
		end)

		ctx:Test("should shake for nearly no time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0
			shake.FadeOutTime = 0
			shake.SustainTime = 0
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0, 0.05)
		end)

		ctx:Test("should shake for fade in time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0.1
			shake.FadeOutTime = 0
			shake.SustainTime = 0
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.1, 0.05)
		end)

		ctx:Test("should shake for fade out time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0
			shake.FadeOutTime = 0.1
			shake.SustainTime = 0
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.1, 0.05)
		end)

		ctx:Test("should shake for sustain time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0
			shake.FadeOutTime = 0
			shake.SustainTime = 0.1
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.1, 0.05)
		end)

		ctx:Test("should shake for fade in and sustain time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0.1
			shake.FadeOutTime = 0
			shake.SustainTime = 0.1
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.2, 0.05)
		end)

		ctx:Test("should shake for fade out and sustain time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0
			shake.FadeOutTime = 0.1
			shake.SustainTime = 0.1
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.2, 0.05)
		end)

		ctx:Test("should shake for fade in and fade out time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0.1
			shake.FadeOutTime = 0.1
			shake.SustainTime = 0
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.2, 0.05)
		end)

		ctx:Test("should shake for fading and sustain time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0.1
			shake.FadeOutTime = 0.1
			shake.SustainTime = 0.1
			shake:Start()
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.3, 0.05)
		end)

		ctx:Test("should shake indefinitely", function()
			local shake = Shake.new()
			shake.FadeInTime = 0
			shake.FadeOutTime = 0
			shake.SustainTime = 0
			shake.Sustain = true
			shake:Start()
			local shakeTime = 0.1
			task.delay(shakeTime, function()
				shake:StopSustain()
			end)
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(shakeTime, 0.05)
		end)

		ctx:Test("should shake indefinitely and fade out", function()
			local shake = Shake.new()
			shake.FadeInTime = 0
			shake.FadeOutTime = 0.1
			shake.SustainTime = 0
			shake.Sustain = true
			shake:Start()
			local shakeTime = 0.1
			task.delay(shakeTime, function()
				shake:StopSustain()
			end)
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.2, 0.05)
		end)

		ctx:Test("should shake indefinitely and fade out with fade in time", function()
			local shake = Shake.new()
			shake.FadeInTime = 0.1
			shake.FadeOutTime = 0.1
			shake.SustainTime = 0
			shake.Sustain = true
			shake:Start()
			local shakeTime = 0.3
			task.delay(shakeTime, function()
				shake:StopSustain()
			end)
			local duration = AwaitStop(shake)
			ctx:Expect(duration):ToBeNear(0.4, 0.05)
		end)

		ctx:Test("should connect to signal", function()
			local shake = Shake.new()
			shake.SustainTime = 0.1
			shake:Start()
			local signaled = false
			local connection = shake:OnSignal(RunService.Heartbeat, function()
				signaled = true
			end)
			ctx:Expect(typeof(connection)):ToBe("RBXScriptConnection")
			ctx:Expect(connection.Connected):ToBe(true)
			AwaitStop(shake)
			ctx:Expect(signaled):ToBe(true)
			ctx:Expect(connection.Connected):ToBe(false)
		end)

		-- RenderStepped only works on the client:
		if RunService:IsClient() and RunService:IsRunning() then
			ctx:Test("should bind to render step", function()
				local shake = Shake.new()
				shake.SustainTime = 0.1
				shake:Start()
				local bound = false
				shake:BindToRenderStep("ShakeTest", Enum.RenderPriority.Last.Value, function()
					bound = true
				end)
				AwaitStop(shake)
				ctx:Expect(bound):ToBe(true)
			end)
		end
	end)
end


-- -----------------------------------------------------------------------------
--               Batched Yield-Safe Signal Implementation                     --
-- This is a Signal class which has effectively identical behavior to a       --
-- normal RBXScriptSignal, with the only difference being a couple extra      --
-- stack frames at the bottom of the stack trace when an error is thrown.     --
-- This implementation caches runner coroutines, so the ability to yield in   --
-- the signal handlers comes at minimal extra cost over a naive signal        --
-- implementation that either always or never spawns a thread.                --
--                                                                            --
-- License:                                                                   --
--   Licensed under the MIT license.                                          --
--                                                                            --
-- Authors:                                                                   --
--   stravant - July 31st, 2021 - Created the file.                           --
--   sleitnick - August 3rd, 2021 - Modified for Knit.                        --
-- -----------------------------------------------------------------------------

-- Signal types
export type Connection = {
	Disconnect: (self: Connection) -> (),
	Destroy: (self: Connection) -> (),
	Connected: boolean,
}

export type Signal<T...> = {
	Fire: (self: Signal<T...>, T...) -> (),
	FireDeferred: (self: Signal<T...>, T...) -> (),
	Connect: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	Once: (self: Signal<T...>, fn: (T...) -> ()) -> Connection,
	DisconnectAll: (self: Signal<T...>) -> (),
	GetConnections: (self: Signal<T...>) -> { Connection },
	Destroy: (self: Signal<T...>) -> (),
	Wait: (self: Signal<T...>) -> T...,
}

-- The currently idle thread to run the next handler on
local freeRunnerThread = nil

-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
	local acquiredRunnerThread = freeRunnerThread
	freeRunnerThread = nil
	fn(...)
	-- The handler finished running, this runner thread is free again.
	freeRunnerThread = acquiredRunnerThread
end

-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
	acquireRunnerThreadAndCallEventHandler(...)
	while true do
		acquireRunnerThreadAndCallEventHandler(coroutine.yield())
	end
end

--[=[
	@within Signal
	@interface SignalConnection
	.Connected boolean
	.Disconnect (SignalConnection) -> ()

	Represents a connection to a signal.
	```lua
	local connection = signal:Connect(function() end)
	print(connection.Connected) --> true
	connection:Disconnect()
	print(connection.Connected) --> false
	```
]=]

-- Connection class
local Connection = {}
Connection.__index = Connection

function Connection:Disconnect()
	if not self.Connected then
		return
	end
	self.Connected = false

	-- Unhook the node, but DON'T clear it. That way any fire calls that are
	-- currently sitting on this node will be able to iterate forwards off of
	-- it, but any subsequent fire calls will not hit it, and it will be GCed
	-- when no more fire calls are sitting on it.
	if self._signal._handlerListHead == self then
		self._signal._handlerListHead = self._next
	else
		local prev = self._signal._handlerListHead
		while prev and prev._next ~= self do
			prev = prev._next
		end
		if prev then
			prev._next = self._next
		end
	end
end

Connection.Destroy = Connection.Disconnect

-- Make Connection strict
setmetatable(Connection, {
	__index = function(_tb, key)
		error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

--[=[
	@within Signal
	@type ConnectionFn (...any) -> ()

	A function connected to a signal.
]=]

--[=[
	@class Signal

	A Signal is a data structure that allows events to be dispatched
	and observed.

	This implementation is a direct copy of the de facto standard, [GoodSignal](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063),
	with some added methods and typings.

	For example:
	```lua
	local signal = Signal.new()

	-- Subscribe to a signal:
	signal:Connect(function(msg)
		print("Got message:", msg)
	end)

	-- Dispatch an event:
	signal:Fire("Hello world!")
	```
]=]
local Signal = {}
Signal.__index = Signal

--[=[
	Constructs a new Signal

	@return Signal
]=]
function Signal.new<T...>(): Signal<T...>
	local self = setmetatable({
		_handlerListHead = false,
		_proxyHandler = nil,
		_yieldedThreads = nil,
	}, Signal)

	return self
end

--[=[
	Constructs a new Signal that wraps around an RBXScriptSignal.

	@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
	@return Signal

	For example:
	```lua
	local signal = Signal.Wrap(workspace.ChildAdded)
	signal:Connect(function(part) print(part.Name .. " added") end)
	Instance.new("Part").Parent = workspace
	```
]=]
function Signal.Wrap<T...>(rbxScriptSignal: RBXScriptSignal): Signal<T...>
	assert(
		typeof(rbxScriptSignal) == "RBXScriptSignal",
		"Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal)
	)

	local signal = Signal.new()
	signal._proxyHandler = rbxScriptSignal:Connect(function(...)
		signal:Fire(...)
	end)

	return signal
end

--[=[
	Checks if the given object is a Signal.

	@param obj any -- Object to check
	@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj: any): boolean
	return type(obj) == "table" and getmetatable(obj) == Signal
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called anytime the signal is fired.
	```lua
	signal:Connect(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	```
]=]
function Signal:Connect(fn)
	local connection = setmetatable({
		Connected = true,
		_signal = self,
		_fn = fn,
		_next = false,
	}, Connection)

	if self._handlerListHead then
		connection._next = self._handlerListHead
		self._handlerListHead = connection
	else
		self._handlerListHead = connection
	end

	return connection
end

--[=[
	@deprecated v1.3.0 -- Use `Signal:Once` instead.
	@param fn ConnectionFn
	@return SignalConnection
]=]
function Signal:ConnectOnce(fn)
	return self:Once(fn)
end

--[=[
	@param fn ConnectionFn
	@return SignalConnection

	Connects a function to the signal, which will be called the next time the signal fires. Once
	the connection is triggered, it will disconnect itself.
	```lua
	signal:Once(function(msg, num)
		print(msg, num)
	end)

	signal:Fire("Hello", 25)
	signal:Fire("This message will not go through", 10)
	```
]=]
function Signal:Once(fn)
	local connection
	local done = false

	connection = self:Connect(function(...)
		if done then
			return
		end

		done = true
		connection:Disconnect()
		fn(...)
	end)

	return connection
end

function Signal:GetConnections()
	local items = {}

	local item = self._handlerListHead
	while item do
		table.insert(items, item)
		item = item._next
	end

	return items
end

-- Disconnect all handlers. Since we use a linked list it suffices to clear the
-- reference to the head handler.
--[=[
	Disconnects all connections from the signal.
	```lua
	signal:DisconnectAll()
	```
]=]
function Signal:DisconnectAll()
	local item = self._handlerListHead
	while item do
		item.Connected = false
		item = item._next
	end
	self._handlerListHead = false

	local yieldedThreads = rawget(self, "_yieldedThreads")
	if yieldedThreads then
		for thread in yieldedThreads do
			if coroutine.status(thread) == "suspended" then
				warn(debug.traceback(thread, "signal disconnected; yielded thread cancelled", 2))
				task.cancel(thread)
			end
		end
		table.clear(self._yieldedThreads)
	end
end

-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
	@param ... any

	Fire the signal, which will call all of the connected functions with the given arguments.
	```lua
	signal:Fire("Hello")

	-- Any number of arguments can be fired:
	signal:Fire("Hello", 32, {Test = "Test"}, true)
	```
]=]
function Signal:Fire(...)
	local item = self._handlerListHead
	while item do
		if item.Connected then
			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
end

--[=[
	@param ... any

	Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
	```lua
	signal:FireDeferred("Hello")
	```
]=]
function Signal:FireDeferred(...)
	local item = self._handlerListHead
	while item do
		local conn = item
		task.defer(function(...)
			if conn.Connected then
				conn._fn(...)
			end
		end, ...)
		item = item._next
	end
end

--[=[
	@return ... any
	@yields

	Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
	Yielding the current thread is not always desirable. If the desire is to only capture the next event
	fired, using `Once` might be a better solution.
	```lua
	task.spawn(function()
		local msg, num = signal:Wait()
		print(msg, num) --> "Hello", 32
	end)
	signal:Fire("Hello", 32)
	```
]=]
function Signal:Wait()
	local yieldedThreads = rawget(self, "_yieldedThreads")
	if not yieldedThreads then
		yieldedThreads = {}
		rawset(self, "_yieldedThreads", yieldedThreads)
	end

	local thread = coroutine.running()
	yieldedThreads[thread] = true

	self:Once(function(...)
		yieldedThreads[thread] = nil

		if coroutine.status(thread) == "suspended" then
			task.spawn(thread, ...)
		end
	end)

	return coroutine.yield()
end

--[=[
	Cleans up the signal.

	Technically, this is only necessary if the signal is created using
	`Signal.Wrap`. Connections should be properly GC'd once the signal
	is no longer referenced anywhere. However, it is still good practice
	to include ways to strictly clean up resources. Calling `Destroy`
	on a signal will also disconnect all connections immediately.
	```lua
	signal:Destroy()
	```
]=]
function Signal:Destroy()
	self:DisconnectAll()

	local proxyHandler = rawget(self, "_proxyHandler")
	if proxyHandler then
		proxyHandler:Disconnect()
	end
end

-- Make signal strict
setmetatable(Signal, {
	__index = function(_tb, key)
		error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
	__newindex = function(_tb, key, _value)
		error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
	end,
})

return table.freeze({
	new = Signal.new,
	Wrap = Signal.Wrap,
	Is = Signal.Is,
})


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

local function AwaitCondition(predicate: () -> boolean, timeout: number?)
	local start = os.clock()
	timeout = (timeout or 10)
	while true do
		if predicate() then
			return true
		end
		if (os.clock() - start) > timeout then
			return false
		end
		task.wait()
	end
end

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

	local signal

	local function NumConns(sig)
		sig = sig or signal
		return #sig:GetConnections()
	end

	ctx:BeforeEach(function()
		signal = Signal.new()
	end)

	ctx:AfterEach(function()
		signal:Destroy()
	end)

	ctx:Describe("Constructor", function()
		ctx:Test("should create a new signal and fire it", function()
			ctx:Expect(Signal.Is(signal)):ToBe(true)
			task.defer(function()
				signal:Fire(10, 20)
			end)
			local n1, n2 = signal:Wait()
			ctx:Expect(n1):ToBe(10)
			ctx:Expect(n2):ToBe(20)
		end)

		ctx:Test("should create a proxy signal and connect to it", function()
			local signalWrap = Signal.Wrap(game:GetService("RunService").Heartbeat)
			ctx:Expect(Signal.Is(signalWrap)):ToBe(true)
			local fired = false
			signalWrap:Connect(function()
				fired = true
			end)
			ctx:Expect(AwaitCondition(function()
				return fired
			end, 2)):ToBe(true)
			signalWrap:Destroy()
		end)
	end)

	ctx:Describe("FireDeferred", function()
		ctx:Test("should be able to fire primitive argument", function()
			local send = 10
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:FireDeferred(send)
			ctx:Expect(AwaitCondition(function()
				return (send == value)
			end, 1)):ToBe(true)
		end)

		ctx:Test("should be able to fire a reference based argument", function()
			local send = { 10, 20 }
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:FireDeferred(send)
			ctx:Expect(AwaitCondition(function()
				return (send == value)
			end, 1)):ToBe(true)
		end)
	end)

	ctx:Describe("Fire", function()
		ctx:Test("should be able to fire primitive argument", function()
			local send = 10
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:Fire(send)
			ctx:Expect(value):ToBe(send)
		end)

		ctx:Test("should be able to fire a reference based argument", function()
			local send = { 10, 20 }
			local value
			signal:Connect(function(v)
				value = v
			end)
			signal:Fire(send)
			ctx:Expect(value):ToBe(send)
		end)
	end)

	ctx:Describe("ConnectOnce", function()
		ctx:Test("should only capture first fire", function()
			local value
			local c = signal:ConnectOnce(function(v)
				value = v
			end)
			ctx:Expect(c.Connected):ToBe(true)
			signal:Fire(10)
			ctx:Expect(c.Connected):ToBe(false)
			signal:Fire(20)
			ctx:Expect(value):ToBe(10)
		end)
	end)

	ctx:Describe("Wait", function()
		ctx:Test("should be able to wait for a signal to fire", function()
			task.defer(function()
				signal:Fire(10, 20, 30)
			end)
			local n1, n2, n3 = signal:Wait()
			ctx:Expect(n1):ToBe(10)
			ctx:Expect(n2):ToBe(20)
			ctx:Expect(n3):ToBe(30)
		end)
	end)

	ctx:Describe("DisconnectAll", function()
		ctx:Test("should disconnect all connections", function()
			signal:Connect(function() end)
			signal:Connect(function() end)
			ctx:Expect(NumConns()):ToBe(2)
			signal:DisconnectAll()
			ctx:Expect(NumConns()):ToBe(0)
		end)
	end)

	ctx:Describe("Disconnect", function()
		ctx:Test("should disconnect connection", function()
			local con = signal:Connect(function() end)
			ctx:Expect(NumConns()):ToBe(1)
			con:Disconnect()
			ctx:Expect(NumConns()):ToBe(0)
		end)

		ctx:Test("should still work if connections disconnected while firing", function()
			local a = 0
			local c
			signal:Connect(function()
				a += 1
			end)
			c = signal:Connect(function()
				c:Disconnect()
				a += 1
			end)
			signal:Connect(function()
				a += 1
			end)
			signal:Fire()
			ctx:Expect(a):ToBe(3)
		end)

		ctx:Test("should still work if connections disconnected while firing deferred", function()
			local a = 0
			local c
			signal:Connect(function()
				a += 1
			end)
			c = signal:Connect(function()
				c:Disconnect()
				a += 1
			end)
			signal:Connect(function()
				a += 1
			end)
			signal:FireDeferred()
			ctx:Expect(AwaitCondition(function()
				return a == 3
			end)):ToBe(true)
		end)
	end)
end


--!strict

-- TableWatcher
-- Stephen Leitnick
-- April 29, 2022

type AnyTable = { [any]: any }

type Watcher = {
	Changes: AnyTable,
	Tbl: AnyTable,
}

local Util = require(script.Parent.Util)

local watchers: { [TableWatcher]: Watcher } = {}
setmetatable(watchers, { __mode = "k" })

local WatcherMt = {}

function WatcherMt:__index(index)
	local w = watchers[self]
	local c = w.Changes[index]
	if c ~= nil then
		if c == Util.None then
			return nil
		else
			return c
		end
	end
	return w.Tbl[index]
end

function WatcherMt:__newindex(index, value)
	local w = watchers[self]
	if w.Tbl[index] == value then
		return
	end
	if value == nil then
		w.Changes[index] = Util.None
	else
		w.Changes[index] = value
	end
end

function WatcherMt:__call()
	local w = watchers[self]
	if next(w.Changes) == nil then
		return w.Tbl
	end
	return Util.Extend(w.Tbl, w.Changes)
end

local function TableWatcher(t: AnyTable): TableWatcher
	local watcher = setmetatable({}, WatcherMt)
	watchers[watcher] = {
		Changes = {},
		Tbl = t,
	}
	return watcher
end

type TableWatcher = typeof(setmetatable({}, WatcherMt))

return TableWatcher


--!strict

-- Util
-- Stephen Leitnick
-- April 29, 2022

type AnyTable = { [any]: any }

local Util = {}

Util.None = newproxy()

-- Recursive table freeze.
function Util.DeepFreeze<T>(tbl: AnyTable): AnyTable
	table.freeze(tbl)
	for _, v in tbl do
		if type(v) == "table" then
			Util.DeepFreeze(v)
		end
	end
	return tbl
end

-- Recursive table copy.
function Util.DeepCopy<T>(tbl: AnyTable): AnyTable
	local newTbl = table.clone(tbl)
	for k, v in newTbl do
		if type(v) == "table" then
			newTbl[k] = Util.DeepCopy(v)
		end
	end
	return newTbl
end

-- Extends one table with another.
-- Similar to the spread operator in JavaScript.
function Util.Extend(original: AnyTable, extension: AnyTable): AnyTable
	local t = Util.DeepCopy(original)
	for k, v in extension do
		if type(v) == "table" then
			if type(original[k]) == "table" then
				t[k] = Util.Extend(original[k], v)
			else
				t[k] = Util.DeepCopy(v)
			end
		elseif v == Util.None then
			t[k] = nil
		else
			t[k] = v
		end
	end
	return t
end

return Util


-- Silo
-- Stephen Leitnick
-- April 29, 2022

--[=[
	@within Silo
	@type State<S> {[string]: any}
	Represents state.
]=]
export type State<S> = S & { [string]: any }

--[=[
	@within Silo
	@type Modifier<S> (State<S>, any) -> ()
	A function that modifies state.
]=]
export type Modifier<S> = (State<S>, any) -> ()

--[=[
	@within Silo
	@interface Action<A>
	.Name string
	.Payload A
	Actions are passed to `Dispatch`. However, typically actions are
	never constructed by hand. Use a silo's Actions table to generate
	these actions.
]=]
type Action<A> = {
	Name: string,
	Payload: A,
}

export type Silo<S, A> = {
	Actions: { [string]: <A>(value: A) -> () },

	GetState: (self: Silo<S, A>) -> State<S>,
	Dispatch: (self: Silo<S, A>, action: Action<A>) -> (),
	ResetToDefaultState: (self: Silo<S, A>) -> (),
	Subscribe: (self: Silo<S, A>, subscriber: (newState: State<S>, oldState: State<S>) -> ()) -> () -> (),
	Watch: <T>(self: Silo<S, A>, selector: (State<S>) -> T, onChange: (T) -> ()) -> () -> (),
}

local TableWatcher = require(script.TableWatcher)
local Util = require(script.Util)

--[=[
	@class Silo
	A Silo is a state container, inspired by Redux slices and
	designed for Roblox developers.
]=]
local Silo = {}
Silo.__index = Silo

--[=[
	@return Silo
	Create a Silo.

	```lua
	local statsSilo = Silo.new({
		-- Initial state:
		Kills = 0,
		Deaths = 0,
		Points = 0,
	}, {
		-- Modifiers are functions that modify the state:
		SetKills = function(state, kills)
			state.Kills = kills
		end,
		AddPoints = function(state, points)
			state.Points += points
		end,
	})

	-- Use Actions to modify the state:
	statsSilo:Dispatch(statsSilo.Actions.SetKills(10))

	-- Use GetState to get the current state:
	print("Kills", statsSilo:GetState().Kills)
	```

	From the above example, note how the modifier functions were transformed
	into functions that can be called from `Actions` with just the single
	payload (no need to pass state). The `SetKills` modifier is then used
	as the `SetKills` action to be dispatched.
]=]
function Silo.new<S, A>(defaultState: State<S>, modifiers: { [string]: Modifier<S> }?): Silo<S, A>
	local self = setmetatable({}, Silo)

	self._DefaultState = Util.DeepFreeze(Util.DeepCopy(defaultState))
	self._State = Util.DeepFreeze(Util.DeepCopy(defaultState))
	self._Modifiers = {} :: { [string]: any }
	self._Dispatching = false
	self._Parent = self
	self._Subscribers = {}

	self.Actions = {}

	-- Create modifiers and action creators:
	if modifiers then
		for actionName, modifier in modifiers do
			self._Modifiers[actionName] = function(state: State<S>, payload: any)
				-- Create a watcher to virtually watch for state mutations:
				local watcher = TableWatcher(state)
				modifier(watcher :: any, payload)
				-- Apply state mutations into new state table:
				return watcher()
			end

			self.Actions[actionName] = function(payload)
				return {
					Name = actionName,
					Payload = payload,
				}
			end
		end
	end

	return self
end

--[=[
	@param silos {Silo}
	@return Silo
	Constructs a new silo as a combination of other silos.
]=]
function Silo.combine<S, A>(silos: { [string]: Silo<unknown, unknown> }, initialState: State<S>?): Silo<S, A>
	-- Combine state:
	local state = {}
	for name, silo in silos do
		if silo._Dispatching then
			error("cannot combine silos from a modifier", 2)
		end
		state[name] = silo:GetState()
	end

	local combinedSilo = Silo.new(Util.Extend(state, initialState or {}))

	-- Combine modifiers and actions:
	for name, silo in silos do
		silo._Parent = combinedSilo
		for actionName, modifier in silo._Modifiers do
			-- Prefix action name to keep it unique:
			local fullActionName = `{name}/{actionName}`
			combinedSilo._Modifiers[fullActionName] = function(s, payload)
				-- Extend the top-level state from the sub-silo state modification:
				return Util.Extend(s, {
					[name] = modifier((s :: { [string]: any })[name], payload),
				})
			end
		end
		for actionName in silo.Actions do
			if combinedSilo.Actions[actionName] ~= nil then
				error(`duplicate action name {actionName} found when combining silos`, 2)
			end
			-- Update the action creator to include the correct prefixed action name:
			local fullActionName = `{name}/{actionName}`
			silo.Actions[actionName] = function(p)
				return {
					Name = fullActionName,
					Payload = p,
				}
			end
			combinedSilo.Actions[actionName] = silo.Actions[actionName]
		end
	end

	return combinedSilo
end

--[=[
	Get the current state.

	```lua
	local state = silo:GetState()
	```
]=]
function Silo:GetState<S>(): State<S>
	if self._Parent ~= self then
		error("can only get state from top-level silo", 2)
	end
	return self._State
end

--[=[
	Dispatch an action.

	```lua
	silo:Dispatch(silo.Actions.DoSomething("something"))
	```
]=]
function Silo:Dispatch<A>(action: Action<A>)
	if self._Dispatching then
		error("cannot dispatch from a modifier", 2)
	end
	if self._Parent ~= self then
		error("can only dispatch from top-level silo", 2)
	end

	-- Find and invoke the modifier to modify current state:
	self._Dispatching = true
	local oldState = self._State
	local newState = oldState
	local modifier = self._Modifiers[action.Name]
	if modifier then
		newState = modifier(newState, action.Payload)
	end
	self._Dispatching = false

	-- State changed:
	if newState ~= oldState then
		self._State = Util.DeepFreeze(newState)

		-- Notify subscribers of state change:
		for _, subscriber in self._Subscribers do
			subscriber(newState, oldState)
		end
	end
end

--[=[
	Subscribe a function to receive all state updates, including
	initial state (subscriber is called immediately).

	Returns an unsubscribe function. Call the function to unsubscribe.

	```lua
	local unsubscribe = silo:Subscribe(function(newState, oldState)
		-- Do something
	end)

	-- Later on, if desired, disconnect the subscription by calling unsubscribe:
	unsubscribe()
	```
]=]
function Silo:Subscribe<S>(subscriber: (newState: State<S>, oldState: State<S>) -> ()): () -> ()
	if self._Dispatching then
		error("cannot subscribe from within a modifier", 2)
	end
	if self._Parent ~= self then
		error("can only subscribe on top-level silo", 2)
	end
	if table.find(self._Subscribers, subscriber) then
		error("cannot subscribe same function more than once", 2)
	end

	table.insert(self._Subscribers, subscriber)

	-- Unsubscribe:
	return function()
		local index = table.find(self._Subscribers, subscriber)
		if not index then
			return
		end
		table.remove(self._Subscribers, index)
	end
end

--[=[
	Watch a specific value within the state, which is selected by the
	`selector` function. The initial value, and any subsequent changes
	grabbed by the selector, will be passed to the `onChange` function.

	Just like `Subscribe`, a function is returned that can be used
	to unsubscribe (i.e. stop watching).

	```lua
	local function SelectPoints(state)
		return state.Statistics.Points
	end

	local unsubscribe = silo:Watch(SelectPoints, function(points)
		print("Points", points)
	end)
	```
]=]
function Silo:Watch<S, T>(selector: (State<S>) -> T, onChange: (T) -> ()): () -> ()
	local value = selector(self:GetState())

	local unsubscribe = self:Subscribe(function(state)
		local newValue = selector(state)
		if newValue == value then
			return
		end
		value = newValue
		onChange(value)
	end)

	-- Call initial onChange after subscription to verify subscription didn't fail:
	onChange(value)

	return unsubscribe
end

--[=[
	Reset the state to the default state that was given in the constructor.

	```lua
	local silo = Silo.new({
		Points = 0,
	}, {
		SetPoints = function(state, points)
			state.Points = points
		end
	})

	silo:Dispatch(silo.Actions.SetPoints(10))

	print(silo:GetState().Points) -- 10

	silo:ResetToDefaultState()

	print(silo:GetState().Points) -- 0
	```
]=]
function Silo:ResetToDefaultState()
	if self._Dispatching then
		error("cannot reset state from within a modifier", 2)
	end

	if self._Parent ~= self then
		error("can only reset state on top-level silo", 2)
	end

	local oldState = self._State

	if self._DefaultState ~= oldState then
		self._State = Util.DeepFreeze(Util.DeepCopy(self._DefaultState))

		for _, subscriber in self._Subscribers do
			subscriber(self._State, oldState)
		end
	end
end

return {
	new = Silo.new,
	combine = Silo.combine,
}


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

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

	local silo1, silo2, rootSilo

	ctx:BeforeEach(function()
		silo1 = Silo.new({
			Kills = 0,
			Deaths = 0,
		}, {
			SetKills = function(state, kills)
				state.Kills = kills
			end,
			IncrementDeaths = function(state, deaths)
				state.Deaths += deaths
			end,
		})
		silo2 = Silo.new({
			Money = 0,
		}, {
			AddMoney = function(state, money)
				state.Money += money
			end,
		})
		rootSilo = Silo.combine({
			Stats = silo1,
			Econ = silo2,
		})
	end)

	ctx:Describe("State", function()
		ctx:Test("should get state properly", function()
			local silo = Silo.new({
				ABC = 10,
			})
			local state = silo:GetState()
			ctx:Expect(state):ToBeA("table")
			ctx:Expect(state.ABC):ToBe(10)
		end)

		ctx:Test("should get state from combined silos", function()
			local state = rootSilo:GetState()
			ctx:Expect(state):ToBeA("table")
			ctx:Expect(state.Stats):ToBeA("table")
			ctx:Expect(state.Econ):ToBeA("table")
			ctx:Expect(state.Stats.Kills):ToBeA("number")
			ctx:Expect(state.Stats.Deaths):ToBeA("number")
			ctx:Expect(state.Econ.Money):ToBeA("number")
		end)

		ctx:Test("should not allow getting state from sub-silo", function()
			ctx:Expect(function()
				silo1:GetState()
			end):ToThrow()
			ctx:Expect(function()
				silo2:GetState()
			end):ToThrow()
		end)

		ctx:Test("should throw error if attempting to modify state directly", function()
			ctx:Expect(function()
				rootSilo:GetState().Stats.Kills = 10
			end):ToThrow()
			ctx:Expect(function()
				rootSilo:GetState().Stats.SomethingNew = 100
			end):ToThrow()
			ctx:Expect(function()
				rootSilo:GetState().Stats = {}
			end):ToThrow()
			ctx:Expect(function()
				rootSilo:GetState().SomethingElse = {}
			end):ToThrow()
		end)
	end)

	ctx:Describe("Dispatch", function()
		ctx:Test("should dispatch", function()
			ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(0)
			rootSilo:Dispatch(silo1.Actions.SetKills(10))
			ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(10)
			rootSilo:Dispatch(silo2.Actions.AddMoney(10))
			rootSilo:Dispatch(silo2.Actions.AddMoney(20))
			ctx:Expect(rootSilo:GetState().Econ.Money):ToBe(30)
		end)

		ctx:Test("should not allow dispatching from a sub-silo", function()
			ctx:Expect(function()
				silo1:Dispatch(silo1.Action.SetKills(0))
			end):ToThrow()
			ctx:Expect(function()
				silo2:Dispatch(silo2.Action.AddMoney(0))
			end):ToThrow()
		end)

		ctx:Test("should not allow dispatching from within a modifier", function()
			ctx:Expect(function()
				local silo
				silo = Silo.new({
					Data = 0,
				}, {
					SetData = function(state, newData)
						state.Data = newData
						silo:Dispatch({ Name = "", Payload = 0 })
					end,
				})
				silo:Dispatch(silo.Actions.SetData(0))
			end):ToThrow()
		end)
	end)

	ctx:Describe("Subscribe", function()
		ctx:Test("should subscribe to a silo", function()
			local new, old
			local n = 0
			local unsubscribe = rootSilo:Subscribe(function(newState, oldState)
				n += 1
				new, old = newState, oldState
			end)
			ctx:Expect(n):ToBe(0)
			rootSilo:Dispatch(silo1.Actions.SetKills(10))
			ctx:Expect(n):ToBe(1)
			ctx:Expect(new):ToBeA("table")
			ctx:Expect(old):ToBeA("table")
			ctx:Expect(new.Stats.Kills):ToBe(10)
			ctx:Expect(old.Stats.Kills):ToBe(0)
			rootSilo:Dispatch(silo1.Actions.SetKills(20))
			ctx:Expect(n):ToBe(2)
			ctx:Expect(new.Stats.Kills):ToBe(20)
			ctx:Expect(old.Stats.Kills):ToBe(10)
			unsubscribe()
			rootSilo:Dispatch(silo1.Actions.SetKills(30))
			ctx:Expect(n):ToBe(2)
		end)

		ctx:Test("should not allow subscribing same function more than once", function()
			local function sub() end
			ctx:Expect(function()
				rootSilo:Subscribe(sub)
			end)
				:Not()
				:ToThrow()
			ctx:Expect(function()
				rootSilo:Subscribe(sub)
			end):ToThrow()
		end)

		ctx:Test("should not allow subscribing to a sub-silo", function()
			ctx:Expect(function()
				silo1:Subscribe(function() end)
			end):ToThrow()
		end)

		ctx:Test("should not allow subscribing from within a modifier", function()
			ctx:Expect(function()
				local silo
				silo = Silo.new({
					Data = 0,
				}, {
					SetData = function(state, newData)
						state.Data = newData
						silo:Subscribe(function() end)
					end,
				})
				silo:Dispatch(silo.Actions.SetData(0))
			end):ToThrow()
		end)
	end)

	ctx:Describe("Watch", function()
		ctx:Test("should watch value changes", function()
			local function SelectMoney(state)
				return state.Econ.Money
			end
			local changes = 0
			local currentMoney = 0
			local unsubscribeWatch = rootSilo:Watch(SelectMoney, function(money)
				changes += 1
				currentMoney = money
			end)
			ctx:Expect(changes):ToBe(1)
			rootSilo:Dispatch(silo2.Actions.AddMoney(10))
			ctx:Expect(changes):ToBe(2)
			ctx:Expect(currentMoney):ToBe(10)
			rootSilo:Dispatch(silo2.Actions.AddMoney(20))
			ctx:Expect(changes):ToBe(3)
			ctx:Expect(currentMoney):ToBe(30)
			rootSilo:Dispatch(silo2.Actions.AddMoney(0))
			ctx:Expect(changes):ToBe(3)
			ctx:Expect(currentMoney):ToBe(30)
			rootSilo:Dispatch(silo1.Actions.SetKills(10))
			ctx:Expect(changes):ToBe(3)
			ctx:Expect(currentMoney):ToBe(30)
			unsubscribeWatch()
			rootSilo:Dispatch(silo2.Actions.AddMoney(10))
			ctx:Expect(changes):ToBe(3)
			ctx:Expect(currentMoney):ToBe(30)
		end)
	end)

	ctx:Describe("ResetToDefaultState", function()
		ctx:Test("should reset the silo to it's default state", function()
			rootSilo:Dispatch(silo1.Actions.SetKills(10))
			rootSilo:Dispatch(silo2.Actions.AddMoney(30))
			ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(10)
			ctx:Expect(rootSilo:GetState().Econ.Money):ToBe(30)
			rootSilo:ResetToDefaultState()
			ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(0)
			ctx:Expect(rootSilo:GetState().Econ.Money):ToBe(0)
		end)
	end)
end


--!strict

--[=[
	@class Spring

	Simulates a critically damped spring. This is mostly just a wrapper around
	[`TweenService:SmoothDamp()`](https://create.roblox.com/docs/reference/engine/classes/TweenService#SmoothDamp).
]=]

local TweenService = game:GetService("TweenService")

local Spring = {}
Spring.__index = Spring

export type Spring<T> = typeof(setmetatable(
	{} :: {
		Current: T,
		Target: T,
		Velocity: T,
		SmoothTime: number,
		MaxSpeed: number,
	},
	Spring
))

--[=[
	Creates a new spring.

	The type `T` can be a `number`, `Vector2`, `Vector3`, or `CFrame`.

	@param initial T -- Initial value
	@param smoothTime number -- Approximate time it takes to reach target
	@param maxSpeed number -- Maximum speed (defaults to infinity)
]=]
function Spring.new<T>(initial: T, smoothTime: number, maxSpeed: number?): Spring<T>
	local self = setmetatable({
		Current = initial,
		Target = initial,
		Velocity = if typeof(initial) == "CFrame" then CFrame.identity else (initial :: any) * 0,
		SmoothTime = smoothTime,
		MaxSpeed = if maxSpeed ~= nil then maxSpeed else math.huge,
	}, Spring)

	return self
end

--[=[
	@within Spring
	@method Update

	Updates the spring. This method should be called continuously, even if the target value hasn't changed.

	```lua
	local newValue = spring:Update(dt)
	```

	@param deltaTime number
	@return T
]=]
function Spring.Update<T>(self: Spring<T>, dt: number): T
	self.Current, self.Velocity =
		TweenService:SmoothDamp(self.Current, self.Target, self.Velocity, self.SmoothTime, self.MaxSpeed, dt)

	return self.Current
end

--[=[
	@within Spring
	@method Impulse

	Impulses the spring.

	```lua
	spring:Impulse(rng:NextUnitVector() * 10)
	```

	@param velocity T
]=]
function Spring.Impulse<T>(self: Spring<T>, velocity: T)
	self.Velocity += velocity :: any
end

--[=[
	@within Spring
	@method Reset

	Resets the spring to the given value.

	@param value T
]=]
function Spring.Reset<T>(self: Spring<T>, value: T)
	self.Current = value
	self.Target = value
	self.Velocity = if typeof(self.Current) == "CFrame" then CFrame.identity else (self.Current :: any) * 0
end

return Spring


--!native
--!strict

--[=[
	@class stream

	The `stream` library allows a stream-like interface abstraction on top of buffers. This is useful
	for reading or writing buffers without having to keep track of the current offset position.

	This library is specifically tailored around fixed-length buffers.

	```lua
	-- Create an empty stream and write some data:
	local s = stream.create(4)
	stream.writeu8(s, 10)
	stream.writeu8(s, 25)
	stream.writeu8(s, 2)
	stream.writeu8(s, 43)

	-- Grab the buffer if needed:
	local buf = stream.buffer(s)

	-- Create a stream from an existing buffer:
	local s = stream.from(buf)
	print(stream.readu8()) -- 10
	print(stream.readu8()) -- 25
	print(stream.readu8()) -- 2
	print(stream.readu8()) -- 43

	-- Move the cursor:
	stream.seek(s, 1)
	print(stream.readu8()) -- 25
	```
]=]

export type Stream = {
	b: buffer,
	l: number,
	o: number,
}

local stream = {}

--[=[
	@within stream

	Create a stream around a new buffer with the given size in bytes.

	This is equivalent to `stream.frombuffer(buffer.create(size))`.
]=]
function stream.create(size: number): Stream
	return stream.frombuffer(buffer.create(size))
end

--[=[
	@within stream

	Create a stream around an existing buffer.
]=]
function stream.frombuffer(buf: buffer): Stream
	return {
		b = buf,
		l = buffer.len(buf),
		o = 0,
	}
end

--[=[
	@within stream

	Create a stream from an existing string.

	This is equivalent to `stream.frombuffer(buffer.fromstring(size))`.
]=]
function stream.fromstring(str: string): Stream
	local buf = buffer.fromstring(str)
	return {
		b = buf,
		l = buffer.len(buf),
		o = 0,
	}
end

-------------------------------------------------------------------------
-- Read

--[=[
	@within stream
]=]
function stream.readu8(s: Stream): number
	local n = buffer.readu8(s.b, s.o)
	s.o += 1
	return n
end

--[=[
	@within stream
]=]
function stream.readi8(s: Stream): number
	local n = buffer.readi8(s.b, s.o)
	s.o += 1
	return n
end

--[=[
	@within stream
]=]
function stream.readu16(s: Stream): number
	local n = buffer.readu16(s.b, s.o)
	s.o += 2
	return n
end

--[=[
	@within stream
]=]
function stream.readi16(s: Stream): number
	local n = buffer.readi16(s.b, s.o)
	s.o += 2
	return n
end

--[=[
	@within stream
]=]
function stream.readu32(s: Stream): number
	local n = buffer.readu32(s.b, s.o)
	s.o += 4
	return n
end

--[=[
	@within stream
]=]
function stream.readi32(s: Stream): number
	local n = buffer.readi32(s.b, s.o)
	s.o += 4
	return n
end

--[=[
	@within stream
]=]
function stream.readf32(s: Stream): number
	local n = buffer.readf32(s.b, s.o)
	s.o += 4
	return n
end

--[=[
	@within stream
]=]
function stream.readf64(s: Stream): number
	local n = buffer.readf64(s.b, s.o)
	s.o += 8
	return n
end

--[=[
	@within stream
]=]
function stream.readstring(s: Stream, count: number): string
	local str = buffer.readstring(s.b, s.o, count)
	s.o += count
	return str
end

--[=[
	@within stream

	Reads a string previously written using `stream.writelstring`. This function
	assumes that the length of the string is written at the beginning as a u32 int.
]=]
function stream.readlstring(s: Stream): string
	local count = stream.readu32(s)
	return stream.readstring(s, count)
end

--[=[
	@within stream

	Equivalent to `stream.readvectorf32`.
]=]
function stream.readvector(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readf32(b, o + 0)
	local y = buffer.readf32(b, o + 4)
	local z = buffer.readf32(b, o + 8)
	s.o += 12
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectorf32(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readf32(b, o + 0)
	local y = buffer.readf32(b, o + 4)
	local z = buffer.readf32(b, o + 8)
	s.o += 12
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectoru32(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readu32(b, o + 0)
	local y = buffer.readu32(b, o + 4)
	local z = buffer.readu32(b, o + 8)
	s.o += 12
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectori32(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readi32(b, o + 0)
	local y = buffer.readi32(b, o + 4)
	local z = buffer.readi32(b, o + 8)
	s.o += 12
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectoru16(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readu16(b, o + 0)
	local y = buffer.readu16(b, o + 2)
	local z = buffer.readu16(b, o + 4)
	s.o += 6
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectori16(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readi16(b, o + 0)
	local y = buffer.readi16(b, o + 2)
	local z = buffer.readi16(b, o + 4)
	s.o += 6
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectoru8(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readu8(b, o + 0)
	local y = buffer.readu8(b, o + 1)
	local z = buffer.readu8(b, o + 2)
	s.o += 3
	return vector.create(x, y, z)
end

--[=[
	@within stream
]=]
function stream.readvectori8(s: Stream): vector
	local b = s.b
	local o = s.o
	local x = buffer.readi8(b, o + 0)
	local y = buffer.readi8(b, o + 1)
	local z = buffer.readi8(b, o + 2)
	s.o += 3
	return vector.create(x, y, z)
end

-------------------------------------------------------------------------
-- Write

--[=[
	@within stream
]=]
function stream.writeu8(s: Stream, n: number)
	buffer.writeu8(s.b, s.o, n)
	s.o += 1
end

--[=[
	@within stream
]=]
function stream.writei8(s: Stream, n: number)
	buffer.writei8(s.b, s.o, n)
	s.o += 1
end

--[=[
	@within stream
]=]
function stream.writeu16(s: Stream, n: number)
	buffer.writeu16(s.b, s.o, n)
	s.o += 2
end

--[=[
	@within stream
]=]
function stream.writei16(s: Stream, n: number)
	buffer.writei16(s.b, s.o, n)
	s.o += 2
end

--[=[
	@within stream
]=]
function stream.writeu32(s: Stream, n: number)
	buffer.writeu32(s.b, s.o, n)
	s.o += 4
end

--[=[
	@within stream
]=]
function stream.writei32(s: Stream, n: number)
	buffer.writei32(s.b, s.o, n)
	s.o += 4
end

--[=[
	@within stream
]=]
function stream.writef32(s: Stream, n: number)
	buffer.writef32(s.b, s.o, n)
	s.o += 4
end

--[=[
	@within stream
]=]
function stream.writef64(s: Stream, n: number)
	buffer.writef64(s.b, s.o, n)
	s.o += 8
end

--[=[
	@within stream
]=]
function stream.writestring(s: Stream, str: string, count: number?)
	buffer.writestring(s.b, s.o, str, count)
	s.o += count or #str
end

--[=[
	@within stream
]=]
function stream.writelstring(s: Stream, str: string, count: number?)
	stream.writeu32(s, count or #str)
	stream.writestring(s, str, count)
end

--[=[
	@within stream

	Equivalent to `stream.writevectorf32`.
]=]
function stream.writevector(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writef32(b, o + 0, v.x)
	buffer.writef32(b, o + 4, v.y)
	buffer.writef32(b, o + 8, v.z)
	s.o += 12
end

--[=[
	@within stream
]=]
function stream.writevectorf32(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writef32(b, o + 0, v.x)
	buffer.writef32(b, o + 4, v.y)
	buffer.writef32(b, o + 8, v.z)
	s.o += 12
end

--[=[
	@within stream
]=]
function stream.writevectoru32(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writeu32(b, o + 0, v.x)
	buffer.writeu32(b, o + 4, v.y)
	buffer.writeu32(b, o + 8, v.z)
	s.o += 12
end

--[=[
	@within stream
]=]
function stream.writevectori32(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writei32(b, o + 0, v.x)
	buffer.writei32(b, o + 4, v.y)
	buffer.writei32(b, o + 8, v.z)
	s.o += 12
end

--[=[
	@within stream
]=]
function stream.writevectoru16(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writeu16(b, o + 0, v.x)
	buffer.writeu16(b, o + 2, v.y)
	buffer.writeu16(b, o + 4, v.z)
	s.o += 6
end

--[=[
	@within stream
]=]
function stream.writevectori16(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writei16(b, o + 0, v.x)
	buffer.writei16(b, o + 2, v.y)
	buffer.writei16(b, o + 4, v.z)
	s.o += 6
end

--[=[
	@within stream
]=]
function stream.writevectoru8(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writeu8(b, o + 0, v.x)
	buffer.writeu8(b, o + 1, v.y)
	buffer.writeu8(b, o + 2, v.z)
	s.o += 3
end

--[=[
	@within stream
]=]
function stream.writevectori8(s: Stream, v: vector)
	local b = s.b
	local o = s.o
	buffer.writei8(b, o + 0, v.x)
	buffer.writei8(b, o + 1, v.y)
	buffer.writei8(b, o + 2, v.z)
	s.o += 3
end

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

--[=[
	@within stream

	Returns the length of the backing buffer.
]=]
function stream.len(s: Stream): number
	return s.l
end

--[=[
	@within stream

	Returns the position of the stream's cursor.
]=]
function stream.pos(s: Stream): number
	return s.o
end

--[=[
	@within stream

	Copy `count` bytes from `source` into `target`. The provided streams' cursors are
	incremented by `count`.
]=]
function stream.copy(target: Stream, source: Stream, count: number)
	buffer.copy(target.b, target.o, source.b, source.o, count)
	target.o += count
	source.o += count
end

--[=[
	@within stream

	Copy `count` bytes from the `source` stream into the `target` buffer. The `source` stream
	cursor is incremented by `count`.
]=]
function stream.copytobuffer(target: buffer, targetOffset: number, source: Stream, count: number)
	buffer.copy(target, targetOffset, source.b, source.o, count)
	source.o += count
end

--[=[
	@within stream

	Copy `count` bytes from the `source` buffer (optionally offset by `sourceOffset`) into the
	`target` stream. The `target` stream cursor is incremented by `count`.
]=]
function stream.copyfrombuffer(target: Stream, source: buffer, sourceOffset: number?, count: number)
	buffer.copy(target.b, target.o, source, sourceOffset, count)
	target.o += count
end

--[=[
	@within stream

	Moves the cursor relative to the beginning of the stream.
]=]
function stream.seek(s: Stream, offset: number)
	s.o = offset
	if s.o < 0 or s.o > s.l then
		error("seek out of bounds", 2)
	end
end

--[=[
	@within stream

	Moves the cursor backward relative to the end of the stream.
]=]
function stream.seekend(s: Stream, offset: number)
	s.o = s.l - offset
	if s.o < 0 or s.o > s.l then
		error("seek out of bounds", 2)
	end
end

--[=[
	@within stream

	Moves the cursor forward relative to the current cursor position.
]=]
function stream.seekforward(s: Stream, offset: number)
	s.o += offset
	if s.o < 0 or s.o > s.l then
		error("seek out of bounds", 2)
	end
end

--[=[
	@within stream

	Moves the cursor backward relative to the current cursor position.
]=]
function stream.seekbackward(s: Stream, offset: number)
	s.o -= offset
	if s.o < 0 or s.o > s.l then
		error("seek out of bounds", 2)
	end
end

--[=[
	@within stream

	Gets the backing buffer for the stream.
]=]
function stream.buffer(s: Stream): buffer
	return s.b
end

--[=[
	@within stream

	Returns the backing buffer as a string.
]=]
function stream.tostring(s: Stream): string
	return buffer.tostring(s.b)
end

--[=[
	@within stream

	Returns `true` if the cursor is at the end of the stream.
]=]
function stream.atend(s: Stream): boolean
	return s.o == s.l
end

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

return table.freeze(stream)


--!strict

-- Streamable
-- Stephen Leitnick
-- March 03, 2021

type StreamableWithInstance = {
	Instance: Instance?,
	[any]: any,
}

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

--[=[
	@within Streamable
	@prop Instance Instance
	The current instance represented by the Streamable. If this
	is being observed, it will always exist. If not currently
	being observed, this will be `nil`.
]=]

--[=[
	@class Streamable
	@client
	Because parts in StreamingEnabled games can stream in and out of existence at
	any point in time, it is hard to write code to interact with them. This is
	where Streamables come into play. Streamables will observe the existence of
	a given instance, and will signal when the instance exists and does not
	exist.

	The API is very simple. Create a Streamable that points to a certain parent
	and looks for a specific child instance (typically a BasePart). Then, call
	the `Observe` method to observe when the instance streams in and out.

	```lua
	local Streamable = require(packages.Streamable).Streamable

	-- Models might take a bit to load, but the model instance
	-- is never removed, thus we can use WaitForChild.
	local model = workspace:WaitForChild("MyModel")

	-- Watch for a specific part in the model:
	local partStreamable = Streamable.new(model, "SomePart")

	partStreamable:Observe(function(part, trove)
		print(part:GetFullName() .. " added")
		-- Run code on the part here.
		-- Use the trove to manage cleanup when the part goes away.
		trove:Add(function()
			-- General cleanup stuff
			print(part.Name .. " removed")
		end)
	end)

	-- Watch for the PrimaryPart of a model to exist:
	local primaryStreamable = Streamable.primary(model)
	primaryStreamable:Observe(function(primary, trove)
		print("Model now has a PrimaryPart:", primary.Name)
		trove:Add(function()
			print("Model's PrimaryPart has been removed")
		end)
	end)

	-- At any given point, accessing the Instance field will
	-- reference the observed part, if it exists:
	if partStreamable.Instance then
		print("Streamable has its instance:", partStreamable.Instance)
	end

	-- When/if done, call Destroy on the streamable, which will
	-- also clean up any observers:
	partStreamable:Destroy()
	primaryStreamable:Destroy()
	```

	For more information on the mechanics of how StreamingEnabled works
	and what sort of behavior to expect, see the
	[Content Streaming](https://developer.roblox.com/en-us/articles/content-streaming#technical-behavior)
	page. It is important to understand that only BaseParts and their descendants are streamed in/out,
	whereas other instances are loaded during the initial client load. It is also important to understand
	that streaming only occurs on the client. The server has immediate access to everything right away.
]=]
local Streamable = {}
Streamable.__index = Streamable

--[=[
	@return Streamable
	@param parent Instance
	@param childName string

	Constructs a Streamable that watches for a direct child of name `childName`
	within the `parent` Instance. Call `Observe` to observe the existence of
	the child within the parent.
]=]
function Streamable.new(parent: Instance, childName: string)
	local self: StreamableWithInstance = {}
	setmetatable(self, Streamable)

	self._trove = Trove.new()
	self._shown = self._trove:Construct(Signal)
	self._shownTrove = Trove.new()
	self._trove:Add(self._shownTrove)

	self.Instance = parent:FindFirstChild(childName)

	local function OnInstanceSet()
		local instance = self.Instance
		if typeof(instance) == "Instance" then
			self._shown:Fire(instance, self._shownTrove)
			self._shownTrove:Connect(instance:GetPropertyChangedSignal("Parent"), function()
				if not instance.Parent then
					self._shownTrove:Clean()
				end
			end)
			self._shownTrove:Add(function()
				if self.Instance == instance then
					self.Instance = nil
				end
			end)
		end
	end

	local function OnChildAdded(child: Instance)
		if child.Name == childName and not self.Instance then
			self.Instance = child
			OnInstanceSet()
		end
	end

	self._trove:Connect(parent.ChildAdded, OnChildAdded)
	if self.Instance then
		OnInstanceSet()
	end

	return self
end

--[=[
	@return Streamable
	@param parent Model

	Constructs a streamable that watches for the PrimaryPart of the
	given `parent` Model.
]=]
function Streamable.primary(parent: Model)
	local self: StreamableWithInstance = {}
	setmetatable(self, Streamable)

	self._trove = Trove.new()
	self._shown = self._trove:Construct(Signal)
	self._shownTrove = Trove.new()
	self._trove:Add(self._shownTrove)

	self.Instance = parent.PrimaryPart

	local function OnPrimaryPartChanged()
		local primaryPart = parent.PrimaryPart
		self._shownTrove:Clean()
		self.Instance = primaryPart
		if primaryPart then
			self._shown:Fire(primaryPart, self._shownTrove)
		end
	end

	self._trove:Connect(parent:GetPropertyChangedSignal("PrimaryPart"), OnPrimaryPartChanged)
	if self.Instance then
		OnPrimaryPartChanged()
	end

	return self
end

--[=[
	@param handler (instance: Instance, trove: Trove) -> nil
	@return Connection

	Observes the instance. The handler is called anytime the
	instance comes into existence, and the trove given is
	cleaned up when the instance goes away.

	To stop observing, disconnect the returned connection.
]=]
function Streamable:Observe(handler)
	if self.Instance then
		task.spawn(handler, self.Instance, self._shownTrove)
	end
	return self._shown:Connect(handler)
end

--[=[
	Destroys the Streamable. Any observers will be disconnected,
	which also means that troves within observers will be cleaned
	up. This should be called when a streamable is no longer needed.
]=]
function Streamable:Destroy()
	self._trove:Destroy()
end

export type Streamable = typeof(Streamable.new(workspace, "X"))

return Streamable


--!strict

-- StreamableUtil
-- Stephen Leitnick
-- March 03, 2021

local Trove = require(script.Parent.Parent.Trove)
local _Streamable = require(script.Parent.Streamable)

type Streamables = { _Streamable.Streamable }
type CompoundHandler = (Streamables, any) -> nil

--[=[
	@class StreamableUtil
	@client
	A utility library for the Streamable class.

	```lua
	local StreamableUtil = require(packages.Streamable).StreamableUtil
	```
]=]
local StreamableUtil = {}

--[=[
	@param streamables {Streamable}
	@param handler ({[child: string]: Instance}, trove: Trove) -> nil
	@return Trove

	Creates a compound streamable around all the given streamables. The compound
	streamable's observer handler will be fired once _all_ the given streamables
	are in existence, and will be cleaned up when _any_ of the streamables
	disappear.

	```lua
	local s1 = Streamable.new(workspace, "Part1")
	local s2 = Streamable.new(workspace, "Part2")

	local compoundTrove = StreamableUtil.Compound({S1 = s1, S2 = s2}, function(streamables, trove)
		local part1 = streamables.S1.Instance
		local part2 = streamables.S2.Instance
		trove:Add(function()
			print("Cleanup")
		end)
	end)
	```
]=]
function StreamableUtil.Compound(streamables: Streamables, handler: CompoundHandler)
	local compoundTrove = Trove.new()
	local observeAllTrove = Trove.new()
	local allAvailable = false
	local function Check()
		if allAvailable then
			return
		end
		for _, streamable in pairs(streamables) do
			if not streamable.Instance then
				return
			end
		end
		allAvailable = true
		handler(streamables, observeAllTrove)
	end
	local function Cleanup()
		if not allAvailable then
			return
		end
		allAvailable = false
		observeAllTrove:Clean()
	end
	for _, streamable in pairs(streamables) do
		compoundTrove:Add(streamable:Observe(function(_child, trove)
			Check()
			trove:Add(Cleanup)
		end))
	end
	compoundTrove:Add(Cleanup)
	return compoundTrove
end

return StreamableUtil


-- Streamable
-- Stephen Leitnick
-- November 08, 2021

return {
	Streamable = require(script.Streamable),
	StreamableUtil = require(script.StreamableUtil),
}


-- Symbol
-- Stephen Leitnick
-- January 04, 2022

--[=[
	@class Symbol

	Represents a unique object.

	Symbols are often used as unique keys in tables. This is useful to avoid possible collisions
	with a key in a table, since the symbol will always be unique and cannot be reconstructed.

	:::note All Unique
	Every creation of a symbol is unique, even if the
	given names are the same.
	:::

	```lua
	local Symbol = require(packages.Symbol)

	-- Create a symbol:
	local symbol = Symbol("MySymbol")

	-- The name is optional:
	local anotherSymbol = Symbol()

	-- Comparison:
	print(symbol == symbol) --> true

	-- All symbol constructions are unique, regardless of the name:
	print(Symbol("Hello") == Symbol("Hello")) --> false

	-- Commonly used as unique keys in a table:
	local DATA_KEY = Symbol("Data")
	local t = {
		-- Can only be accessed using the DATA_KEY symbol:
		[DATA_KEY] = {}
	}

	print(t[DATA_KEY]) --> {}
	```
]=]

local function Symbol(name: string?)
	local symbol = newproxy(true)
	if not name then
		name = ""
	end
	getmetatable(symbol).__tostring = function()
		return "Symbol(" .. name .. ")"
	end
	return symbol
end

return Symbol


local ServerScriptService = game:GetService("ServerScriptService")

local Test = require(ServerScriptService.TestRunner.Test)

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

	ctx:Describe("Constructor", function()
		ctx:Test("should create a new symbol", function()
			local symbol = Symbol("Test")
			ctx:Expect(symbol):ToBeA("userdata")
			ctx:Expect(symbol == symbol):ToBe(true)
			ctx:Expect(tostring(symbol)):ToBe("Symbol(Test)")
		end)

		ctx:Test("should create a new symbol with no name", function()
			local symbol = Symbol()
			ctx:Expect(symbol):ToBeA("userdata")
			ctx:Expect(symbol == symbol):ToBe(true)
			ctx:Expect(tostring(symbol)):ToBe("Symbol()")
		end)

		ctx:Test("should be unique regardless of the name", function()
			ctx:Expect(Symbol("Test") == Symbol("Test")):ToBe(false)
			ctx:Expect(Symbol() == Symbol()):ToBe(false)
			ctx:Expect(Symbol("Test") == Symbol()):ToBe(false)
			ctx:Expect(Symbol("Test1") == Symbol("Test2")):ToBe(false)
		end)

		ctx:Test("should be useable as a table key", function()
			local symbol = Symbol()
			local t = {}
			t[symbol] = 100
			ctx:Expect(t[symbol]):ToBe(100)
		end)
	end)
end


--!strict

-- TableUtil
-- Stephen Leitnick
-- September 13, 2017

--[=[
	@class TableUtil

	A collection of helpful table utility functions. Many of these functions are carried over from JavaScript or
	Python that are not present in Lua.

	Tables that only work specifically with arrays or dictionaries are marked as such in the documentation.

	:::info Immutability
	All functions (_except_ `SwapRemove`, `SwapRemoveFirstValue`, and `Lock`) treat tables as immutable and will return
	copies of the given table(s) with the operations performed on the copies.
	:::
]=]
local TableUtil = {}

local HttpService = game:GetService("HttpService")
local rng = Random.new()

--[=[
	@within TableUtil
	@function Copy
	@param tbl table -- Table to copy
	@param deep boolean? -- Whether or not to perform a deep copy
	@return table

	Creates a copy of the given table. By default, a shallow copy is
	performed. For deep copies, a second boolean argument must be
	passed to the function.

	:::caution No cyclical references
	Deep copies are _not_ protected against cyclical references. Passing
	a table with cyclical references _and_ the `deep` parameter set to
	`true` will result in a stack-overflow.
	:::
]=]
local function Copy<T>(t: T, deep: boolean?): T
	if not deep then
		return (table.clone(t :: any) :: any) :: T
	end
	local function DeepCopy(tbl: { any })
		local tCopy = table.clone(tbl)
		for k, v in tCopy do
			if type(v) == "table" then
				tCopy[k] = DeepCopy(v)
			end
		end
		return tCopy
	end
	return DeepCopy(t :: any) :: T
end

--[=[
	@within TableUtil
	@function Sync
	@param srcTbl table -- Source table
	@param templateTbl table -- Template table
	@return table

	Synchronizes the `srcTbl` based on the `templateTbl`. This will make
	sure that `srcTbl` has all of the same keys as `templateTbl`, including
	removing keys in `srcTbl` that are not present in `templateTbl`. This
	is a _deep_ operation, so any nested tables will be synchronized as
	well.

	```lua
	local template = {kills = 0, deaths = 0, xp = 0}
	local data = {kills = 10, experience = 12}
	data = TableUtil.Sync(data, template)
	print(data) --> {kills = 10, deaths = 0, xp = 0}
	```

	:::caution Data Loss Warning
	This is a two-way sync, so the source table will have data
	_removed_ that isn't found in the template table. This can
	be problematic if used for player data, where there might
	be dynamic data added that isn't in the template.

	For player data, use `TableUtil.Reconcile` instead.
	:::
]=]
local function Sync<S, T>(srcTbl: S, templateTbl: T): T
	assert(type(srcTbl) == "table", "First argument must be a table")
	assert(type(templateTbl) == "table", "Second argument must be a table")

	local tbl = table.clone(srcTbl)

	-- If 'tbl' has something 'templateTbl' doesn't, then remove it from 'tbl'
	-- If 'tbl' has something of a different type than 'templateTbl', copy from 'templateTbl'
	-- If 'templateTbl' has something 'tbl' doesn't, then add it to 'tbl'
	for k, v in pairs(tbl) do
		local vTemplate = templateTbl[k]

		-- Remove keys not within template:
		if vTemplate == nil then
			tbl[k] = nil

			-- Synchronize data types:
		elseif type(v) ~= type(vTemplate) then
			if type(vTemplate) == "table" then
				tbl[k] = Copy(vTemplate, true)
			else
				tbl[k] = vTemplate
			end

			-- Synchronize sub-tables:
		elseif type(v) == "table" then
			tbl[k] = Sync(v, vTemplate)
		end
	end

	-- Add any missing keys:
	for k, vTemplate in pairs(templateTbl) do
		local v = tbl[k]

		if v == nil then
			if type(vTemplate) == "table" then
				tbl[k] = Copy(vTemplate, true)
			else
				tbl[k] = vTemplate
			end
		end
	end

	return (tbl :: any) :: T
end

--[=[
	@within TableUtil
	@function Reconcile
	@param source table
	@param template table
	@return table

	Performs a one-way sync on the `source` table against the
	`template` table. Any keys found in `template` that are
	not found in `source` will be added to `source`. This is
	useful for syncing player data against data template tables
	to ensure players have all the necessary keys, while
	maintaining existing keys that may no longer be in the
	template.

	This is a deep operation, so nested tables will also be
	properly reconciled.

	```lua
	local template = {kills = 0, deaths = 0, xp = 0}
	local data = {kills = 10, abc = 20}
	local correctedData = TableUtil.Reconcile(data, template)

	print(correctedData) --> {kills = 10, deaths = 0, xp = 0, abc = 20}
	```
]=]
local function Reconcile<S, T>(src: S, template: T): S & T
	assert(type(src) == "table", "First argument must be a table")
	assert(type(template) == "table", "Second argument must be a table")

	local tbl = table.clone(src)

	for k, v in template do
		local sv = src[k]
		if sv == nil then
			if type(v) == "table" then
				tbl[k] = Copy(v, true)
			else
				tbl[k] = v
			end
		elseif type(sv) == "table" then
			if type(v) == "table" then
				tbl[k] = Reconcile(sv, v)
			else
				tbl[k] = Copy(sv, true)
			end
		end
	end

	return (tbl :: any) :: S & T
end

--[=[
	@within TableUtil
	@function SwapRemove
	@param tbl table -- Array
	@param i number -- Index

	Removes index `i` in the table by swapping the value at `i` with
	the last value in the array, and then trimming off the last
	value from the array.

	This allows removal of the value at `i` in `O(1)` time, but does
	not preserve array ordering. If a value needs to be removed from
	an array, but ordering of the array does not matter, using
	`SwapRemove` is always preferred over `table.remove`.

	In the following example, we remove "B" at index 2. SwapRemove does
	this by moving the last value "E" over top of "B", and then trimming
	off "E" at the end of the array:
	```lua
	local t = {"A", "B", "C", "D", "E"}
	TableUtil.SwapRemove(t, 2) -- Remove "B"
	print(t) --> {"A", "E", "C", "D"}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function SwapRemove<T>(t: { T }, i: number)
	local n = #t
	t[i] = t[n]
	t[n] = nil
end

--[=[
	@within TableUtil
	@function SwapRemoveFirstValue
	@param tbl table -- Array
	@param v any -- Value to find
	@return number?

	Performs `table.find(tbl, v)` to find the index of the given
	value, and then performs `TableUtil.SwapRemove` on that index.

	```lua
	local t = {"A", "B", "C", "D", "E"}
	TableUtil.SwapRemoveFirstValue(t, "C")
	print(t) --> {"A", "B", "E", "D"}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function SwapRemoveFirstValue<T>(t: { T }, v: T): number?
	local index: number? = table.find(t, v)
	if index then
		SwapRemove(t, index)
	end
	return index
end

--[=[
	@within TableUtil
	@function Map
	@param tbl table
	@param predicate (value: any, key: any, tbl: table) -> newValue: any
	@return table

	Performs a map operation against the given table, which can be used to
	map new values based on the old values at given keys/indices.

	For example:

	```lua
	local t = {A = 10, B = 20, C = 30}
	local t2 = TableUtil.Map(t, function(value)
		return value * 2
	end)
	print(t2) --> {A = 20, B = 40, C = 60}
	```
]=]
local function Map<T, M>(t: { T }, f: (T, number, { T }) -> M): { M }
	assert(type(t) == "table", "First argument must be a table")
	assert(type(f) == "function", "Second argument must be a function")
	local newT = table.create(#t)
	for k, v in t do
		newT[k] = f(v, k, t)
	end
	return newT
end

--[=[
	@within TableUtil
	@function Filter
	@param tbl table
	@param predicate (value: any, key: any, tbl: table) -> keep: boolean
	@return table

	Performs a filter operation against the given table, which can be used to
	filter out unwanted values from the table.

	For example:

	```lua
	local t = {A = 10, B = 20, C = 30}
	local t2 = TableUtil.Filter(t, function(value, key)
		return value > 15
	end)
	print(t2) --> {B = 40, C = 60}
	```
]=]
local function Filter<T>(t: { T }, predicate: (T, any, { T }) -> boolean): { T }
	assert(type(t) == "table", "First argument must be a table")
	assert(type(predicate) == "function", "Second argument must be a function")
	local newT = table.create(#t)
	if #t > 0 then
		local n = 0
		for i, v in t do
			if predicate(v, i, t) then
				n += 1
				newT[n] = v
			end
		end
	else
		for k, v in t do
			if predicate(v, k, t) then
				newT[k] = v
			end
		end
	end
	return newT
end

--[=[
	@within TableUtil
	@function Reduce
	@param tbl table
	@param predicate (accumulator: any, value: any, index: any, tbl: table) -> result: any
	@return table

	Performs a reduce operation against the given table, which can be used to
	reduce the table into a single value. This could be used to sum up a table
	or transform all the values into a compound value of any kind.

	For example:

	```lua
	local t = {10, 20, 30, 40}
	local result = TableUtil.Reduce(t, function(accum, value)
		return accum + value
	end)
	print(result) --> 100
	```
]=]
local function Reduce<T, R>(t: { T }, predicate: (R, T, any, { T }) -> R, init: R): R
	assert(type(t) == "table", "First argument must be a table")
	assert(type(predicate) == "function", "Second argument must be a function")
	local result = init :: R
	if #t > 0 then
		local start = 1
		if init == nil then
			result = (t[1] :: any) :: R
			start = 2
		end
		for i = start, #t do
			result = predicate(result, t[i], i, t)
		end
	else
		local start = nil
		if init == nil then
			result = (next(t) :: any) :: R
			start = result
		end
		for k, v in next, t, start :: any? do
			result = predicate(result, v, k, t)
		end
	end
	return result
end

--[=[
	@within TableUtil
	@function Assign
	@param target table
	@param ... table
	@return table

	Copies all values of the given tables into the `target` table.

	```lua
	local t = {A = 10}
	local t2 = {B = 20}
	local t3 = {C = 30, D = 40}
	local newT = TableUtil.Assign(t, t2, t3)
	print(newT) --> {A = 10, B = 20, C = 30, D = 40}
	```
]=]
local function Assign<T>(target: { T }, ...: { any }): { T } & { any }
	local tbl = table.clone(target)
	for _, src in { ... } do
		for k, v in src do
			tbl[k] = v
		end
	end
	return tbl
end

--[=[
	@within TableUtil
	@function Extend
	@param target table
	@param extension table
	@return table

	Extends the target array with the extension array.

	```lua
	local t = {10, 20, 30}
	local t2 = {30, 40, 50}
	local tNew = TableUtil.Extend(t, t2)
	print(tNew) --> {10, 20, 30, 30, 40, 50}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function Extend<T, E>(target: { T }, extension: { E }): { T } & { E }
	local tbl = table.clone(target) :: { any }
	for _, v in extension do
		table.insert(tbl, v)
	end
	return tbl
end

--[=[
	@within TableUtil
	@function Reverse
	@param tbl table
	@return table

	Reverses the array.

	```lua
	local t = {1, 5, 10}
	local tReverse = TableUtil.Reverse(t)
	print(tReverse) --> {10, 5, 1}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function Reverse<T>(tbl: { T }): { T }
	local n = #tbl
	local tblRev = table.create(n)
	for i = 1, n do
		tblRev[i] = tbl[n - i + 1]
	end
	return tblRev
end

--[=[
	@within TableUtil
	@function Shuffle
	@param tbl table
	@param rngOverride Random?
	@return table

	Shuffles the table.

	```lua
	local t = {1, 2, 3, 4, 5, 6, 7, 8, 9}
	local shuffled = TableUtil.Shuffle(t)
	print(shuffled) --> e.g. {9, 4, 6, 7, 3, 1, 5, 8, 2}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function Shuffle<T>(tbl: { T }, rngOverride: Random?): { T }
	assert(type(tbl) == "table", "First argument must be a table")
	local shuffled = table.clone(tbl)
	local random = if typeof(rngOverride) == "Random" then rngOverride else rng
	for i = #tbl, 2, -1 do
		local j = random:NextInteger(1, i)
		shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
	end
	return shuffled
end

--[=[
	@within TableUtil
	@function Sample
	@param tbl table
	@param sampleSize number
	@param rngOverride Random?
	@return table

	Returns a random sample of the table.

	```lua
	local t = {1, 2, 3, 4, 5, 6, 7, 8, 9}
	local sample = TableUtil.Sample(t, 3)
	print(sample) --> e.g. {6, 2, 5}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function Sample<T>(tbl: { T }, size: number, rngOverride: Random?): { T }
	assert(type(tbl) == "table", "First argument must be a table")
	assert(type(size) == "number", "Second argument must be a number")

	-- If given table is empty, just return a new empty table:
	local len = #tbl
	if len == 0 then
		return {}
	end

	local shuffled = table.clone(tbl)
	local sample = table.create(size)
	local random = if typeof(rngOverride) == "Random" then rngOverride else rng

	-- Clamp sample size to be no larger than the given table size:
	size = math.clamp(size, 1, len)

	for i = 1, size do
		local j = random:NextInteger(i, len)
		shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
	end

	table.move(shuffled, 1, size, 1, sample)

	return sample
end

--[=[
	@within TableUtil
	@function Flat
	@param tbl table
	@param depth number?
	@return table

	Returns a new table where all sub-arrays have been
	bubbled up to the top. The depth at which the scan
	is performed is dictated by the `depth` parameter,
	which is set to `1` by default.

	```lua
	local t = {{10, 20}, {90, 100}, {30, 15}}
	local flat = TableUtil.Flat(t)
	print(flat) --> {10, 20, 90, 100, 30, 15}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function Flat<T>(tbl: { T }, depth: number?): { T }
	local maxDepth: number = if type(depth) == "number" then depth else 1
	local flatTbl = table.create(#tbl)
	local function Scan(t: { any }, d: number)
		for _, v in t do
			if type(v) == "table" and d < maxDepth then
				Scan(v, d + 1)
			else
				table.insert(flatTbl, v)
			end
		end
	end
	Scan(tbl, 0)
	return flatTbl
end

--[=[
	@within TableUtil
	@function FlatMap
	@param tbl table
	@param predicate (key: any, value: any, tbl: table) -> newValue: any
	@return table

	Calls `TableUtil.Map` on the given table and predicate, and then
	calls `TableUtil.Flat` on the result from the map operation.

	```lua
	local t = {10, 20, 30}
	local result = TableUtil.FlatMap(t, function(value)
		return {value, value * 2}
	end)
	print(result) --> {10, 20, 20, 40, 30, 60}
	```

	:::note Arrays only
	This function works on arrays, but not dictionaries.
	:::
]=]
local function FlatMap<T, M>(tbl: { T }, callback: (T, number, { T }) -> M): { M }
	return Flat(Map(tbl, callback))
end

--[=[
	@within TableUtil
	@function Keys
	@param tbl table
	@return table

	Returns an array with all the keys in the table.

	```lua
	local t = {A = 10, B = 20, C = 30}
	local keys = TableUtil.Keys(t)
	print(keys) --> {"A", "B", "C"}
	```

	:::caution Ordering
	The ordering of the keys is never guaranteed. If order is imperative, call
	`table.sort` on the resulting `keys` array.
	```lua
	local keys = TableUtil.Keys(t)
	table.sort(keys)
	```
	:::
]=]
local function Keys<K, V>(tbl: { [K]: V }): { K }
	local keys = table.create(#tbl)
	for k in tbl do
		table.insert(keys, k)
	end
	return keys
end

--[=[
	@within TableUtil
	@function Values
	@param tbl table
	@return table

	Returns an array with all the values in the table.

	```lua
	local t = {A = 10, B = 20, C = 30}
	local values = TableUtil.Values(t)
	print(values) --> {10, 20, 30}
	```

	:::caution Ordering
	The ordering of the values is never guaranteed. If order is imperative, call
	`table.sort` on the resulting `values` array.
	```lua
	local values = TableUtil.Values(t)
	table.sort(values)
	```
	:::
]=]
local function Values<K, V>(tbl: { [K]: V }): { V }
	local values = table.create(#tbl)
	for _, v in tbl do
		table.insert(values, v)
	end
	return values
end

--[=[
	@within TableUtil
	@function Find
	@param tbl table
	@param callback (value: any, index: any, tbl: table) -> boolean
	@return (value: any?, key: any?)

	Performs a linear scan across the table and calls `callback` on
	each item in the array. Returns the value and key of the first
	pair in which the callback returns `true`.

	```lua
	local t = {
		{Name = "Bob", Age = 20};
		{Name = "Jill", Age = 30};
		{Name = "Ann", Age = 25};
	}

	-- Find first person who has a name starting with J:
	local firstPersonWithJ = TableUtil.Find(t, function(person)
		return person.Name:sub(1, 1):lower() == "j"
	end)

	print(firstPersonWithJ) --> {Name = "Jill", Age = 30}
	```

	:::caution Dictionary Ordering
	While `Find` can also be used with dictionaries, dictionary ordering is never
	guaranteed, and thus the result could be different if there are more
	than one possible matches given the data and callback function.
	:::
]=]
local function Find<K, V>(tbl: { [K]: V }, callback: (V, K, { [K]: V }) -> boolean): (V?, K?)
	for k, v in tbl do
		if callback(v, k, tbl) then
			return v, k
		end
	end
	return nil, nil
end

--[=[
	@within TableUtil
	@function Every
	@param tbl table
	@param callback (value: any, index: any, tbl: table) -> boolean
	@return boolean

	Returns `true` if the `callback` also returns `true` for _every_
	item in the table.

	```lua
	local t = {10, 20, 40, 50, 60}

	local allAboveZero = TableUtil.Every(t, function(value)
		return value > 0
	end)

	print("All above zero:", allAboveZero) --> All above zero: true
	```
]=]
local function Every<K, V>(tbl: { [K]: V }, callback: (V, K, { [K]: V }) -> boolean): boolean
	for k, v in tbl do
		if not callback(v, k, tbl) then
			return false
		end
	end
	return true
end

--[=[
	@within TableUtil
	@function Some
	@param tbl table
	@param callback (value: any, index: any, tbl: table) -> boolean
	@return boolean

	Returns `true` if the `callback` also returns `true` for _at least
	one_ of the items in the table.

	```lua
	local t = {10, 20, 40, 50, 60}

	local someBelowTwenty = TableUtil.Some(t, function(value)
		return value < 20
	end)

	print("Some below twenty:", someBelowTwenty) --> Some below twenty: true
	```
]=]
local function Some<K, V>(tbl: { [K]: V }, callback: (V, K, { [K]: V }) -> boolean): boolean
	for k, v in tbl do
		if callback(v, k, tbl) then
			return true
		end
	end
	return false
end

--[=[
	@within TableUtil
	@function Truncate
	@param tbl table
	@param length number
	@return table

	Returns a new table truncated to the length of `length`. Any length
	equal or greater than the current length will simply return a
	shallow copy of the table.

	```lua
	local t = {10, 20, 30, 40, 50, 60, 70, 80}
	local tTruncated = TableUtil.Truncate(t, 3)
	print(tTruncated) --> {10, 20, 30}
	```
]=]
local function Truncate<T>(tbl: { T }, len: number): { T }
	local n = #tbl
	len = math.clamp(len, 1, n)
	return if len == n then table.clone(tbl) else table.move(tbl, 1, len, 1, table.create(len))
end

--[=[
	@within TableUtil
	@function Zip
	@param ... table
	@return (iter: (t: table, k: any) -> (key: any?, values: table?), tbl: table, startIndex: any?)

	Returns an iterator that can scan through multiple tables at the same time side-by-side, matching
	against shared keys/indices.

	```lua
	local t1 = {10, 20, 30, 40, 50}
	local t2 = {60, 70, 80, 90, 100}

	for key,values in TableUtil.Zip(t1, t2) do
		print(key, values)
	end

	--[[
		Outputs:
		1 {10, 60}
		2 {20, 70}
		3 {30, 80}
		4 {40, 90}
		5 {50, 100}
	--]]
	```
]=]
local function Zip(...: { [any]: any }): ((t: { any }, k: any) -> (any, any), { any }, any)
	assert(select("#", ...) > 0, "Must supply at least 1 table")
	local function ZipIteratorArray(all: { any }, k: number): (number?, { any }?)
		k += 1
		local values = {}
		for i, t in all do
			local v = t[k]
			if v ~= nil then
				values[i] = v
			else
				return nil, nil
			end
		end
		return k, values
	end
	local function ZipIteratorMap(all: { [any]: any }, k: any): (number?, { any }?)
		local values = {}
		for i, t in all do
			local v = next(t, k)
			if v ~= nil then
				values[i] = v
			else
				return nil, nil
			end
		end
		return k, values
	end
	local all = { ... }
	if #all[1] > 0 then
		return ZipIteratorArray, all, 0
	else
		return ZipIteratorMap, all, nil
	end
end

--[=[
	@within TableUtil
	@function Lock
	@param tbl table
	@return table

	Locks the table using `table.freeze`, as well as any
	nested tables within the given table. This will lock
	the whole deep structure of the table, disallowing any
	further modifications.

	```lua
	local tbl = {xyz = {abc = 32}}
	tbl.xyz.abc = 28 -- Works fine
	TableUtil.Lock(tbl)
	tbl.xyz.abc = 64 -- Will throw an error (cannot modify readonly table)
	```
]=]
local function Lock<T>(tbl: T): T
	local function Freeze(t: { [any]: any })
		for k, v in pairs(t) do
			if type(v) == "table" then
				t[k] = Freeze(v)
			end
		end
		return table.freeze(t)
	end
	return Freeze(tbl :: any)
end

--[=[
	@within TableUtil
	@function IsEmpty
	@param tbl table
	@return boolean

	Returns `true` if the given table is empty. This is
	simply performed by checking if `next(tbl)` is `nil`
	and works for both arrays and dictionaries. This is
	useful when needing to check if a table is empty but
	not knowing if it is an array or dictionary.

	```lua
	TableUtil.IsEmpty({}) -- true
	TableUtil.IsEmpty({"abc"}) -- false
	TableUtil.IsEmpty({abc = 32}) -- false
	```
]=]
local function IsEmpty(tbl: { any }): boolean
	return next(tbl) == nil
end

--[=[
	@within TableUtil
	@function EncodeJSON
	@param value any
	@return string

	Proxy for [`HttpService:JSONEncode`](https://developer.roblox.com/en-us/api-reference/function/HttpService/JSONEncode).
]=]
local function EncodeJSON(value: any): string
	return HttpService:JSONEncode(value)
end

--[=[
	@within TableUtil
	@function DecodeJSON
	@param value any
	@return string

	Proxy for [`HttpService:JSONDecode`](https://developer.roblox.com/en-us/api-reference/function/HttpService/JSONDecode).
]=]
local function DecodeJSON(str: string): any
	return HttpService:JSONDecode(str)
end

TableUtil.Copy = Copy
TableUtil.Sync = Sync
TableUtil.Reconcile = Reconcile
TableUtil.SwapRemove = SwapRemove
TableUtil.SwapRemoveFirstValue = SwapRemoveFirstValue
TableUtil.Map = Map
TableUtil.Filter = Filter
TableUtil.Reduce = Reduce
TableUtil.Assign = Assign
TableUtil.Extend = Extend
TableUtil.Reverse = Reverse
TableUtil.Shuffle = Shuffle
TableUtil.Sample = Sample
TableUtil.Flat = Flat
TableUtil.FlatMap = FlatMap
TableUtil.Keys = Keys
TableUtil.Values = Values
TableUtil.Find = Find
TableUtil.Every = Every
TableUtil.Some = Some
TableUtil.Truncate = Truncate
TableUtil.Zip = Zip
TableUtil.Lock = Lock
TableUtil.IsEmpty = IsEmpty
TableUtil.EncodeJSON = EncodeJSON
TableUtil.DecodeJSON = DecodeJSON

return TableUtil