game:DefineFastFlag("DebugReceiveLikelySpeakingUsers", false);
return function()
    return game:GetFastFlag("DebugReceiveLikelySpeakingUsers");
end;


game:DefineFastFlag("SetActiveWhenConnecting", false);
return function()
    return game:GetFastFlag("SetActiveWhenConnecting");
end;


game:DefineFastFlag("UpdateDeviceInputPlayerChanged", false);
return function()
    return game:GetFastFlag("UpdateDeviceInputPlayerChanged");
end;


game:DefineFastFlag("UseAudioInstanceAdded", false);
return function()
    return game:GetFastFlag("UseAudioInstanceAdded") and game:GetEngineFeature("AudioInstanceAddedApiEnabled");
end;


local l_Url_0 = require(script:FindFirstAncestor("VoiceChatCore").Parent.Http).Url;
return function(v1, v2, v3)
    assert(type(v3) == "string", "GetShowAgeVerificationOverlay request expects placeId to be a string");
    assert(type(v2) == "string", "GetShowAgeVerificationOverlay request expects universeId to be a string");
    return v1(string.format("%s/v1/settings/verify/show-age-verification-overlay/%s", l_Url_0.VOICE_URL, if v3 ~= "" and v3 ~= nil then v2 .. "?placeId=" .. v3 else v2), "GET");
end;


local l_HttpService_0 = game:GetService("HttpService");
local l_Players_0 = game:GetService("Players");
local v2 = {};
v2.__index = v2;
v2.new = function()
    return (setmetatable({
        listeners = {}
    }, v2));
end;
v2.addListener = function(v3, v4, v5)
    if not v3.listeners[v4] then
        v3.listeners[v4] = {};
    end;
    table.insert(v3.listeners[v4], v5);
end;
v2.removeAllListeners = function(v6)
    table.clear(v6.listeners);
end;
v2.emit = function(v7, v8, ...)
    local v9 = v7.listeners[v8];
    if v9 then
        for _, v11 in (table.clone(v9)), nil, nil do
            pcall(function(...)
                v11(...);
                return nil;
            end, ...);
        end;
        return ;
    else
        return ;
    end;
end;
return {
    jsonDecode = function(v12)
        local l_status_0, l_result_0 = pcall(function()
            return l_HttpService_0:JSONDecode(v12);
        end);
        if not l_status_0 or not l_result_0 then
            return {};
        else
            return l_result_0;
        end;
    end,
    EventEmitter = v2,
    shorten = function(v15)
        return "..." .. string.sub(tostring(v15), -4);
    end,
    avoidEmitting = function(v16)
        v16.DescendantAdded:Connect(function(v17)
            if v17:HasTag("RbxDefaultVoiceEmitter") then
                task.defer(function()
                    v17:Destroy();
                end);
            end;
        end);
        for _, v19 in v16:GetDescendants() do
            if v19:HasTag("RbxDefaultVoiceEmitter") then
                task.defer(function()
                    v19:Destroy();
                end);
            end;
        end;
    end,
    destroyDefaultEmitter = function(v20)
        local l_LocalPlayer_0 = l_Players_0.LocalPlayer;
        if l_LocalPlayer_0 then
            local l_Character_0 = l_LocalPlayer_0.Character;
            if l_Character_0 then
                if v20:IsDescendantOf(l_Character_0) then
                    task.defer(function()
                        v20:Destroy();
                    end);
                end;
                return ;
            else
                return ;
            end;
        else
            return ;
        end;
    end,
    bind = function(v23, v24)
        return function(...)
            return v23[v24](v23, ...);
        end;
    end
};


local v0 = require(script.CoreVoiceManager);
local v1 = require(script.Analytics);
local v2 = require(script.Constants);
local l_Requests_0 = script.Requests;
local l_Flags_0 = script.Flags;
return {
    ["jest.config"] = script["jest.config"],
    CoreVoiceManager = v0,
    Constants = v2,
    Analytics = v1,
    Flags = {
        GetFFlagAlwaysSetupVoiceListeners = l_Flags_0.GetFFlagAlwaysSetupVoiceListeners,
        GetFFlagFixNewAudioAPIEcho = l_Flags_0.GetFFlagFixNewAudioAPIEcho,
        GetFFlagHideUIWhenVoiceDefaultDisabled = l_Flags_0.GetFFlagHideUIWhenVoiceDefaultDisabled,
        GetFFlagHideVoiceUIUntilInputExists = l_Flags_0.GetFFlagHideVoiceUIUntilInputExists,
        GetFFlagOverwriteIsMutedLocally = l_Flags_0.GetFFlagOverwriteIsMutedLocally,
        GetFFlagReceiveLikelySpeakingUsers = l_Flags_0.GetFFlagReceiveLikelySpeakingUsers,
        GetFFlagSetActiveWhenConnecting = l_Flags_0.GetFFlagSetActiveWhenConnecting,
        GetFFlagUpdateDeviceInputPlayerChanged = l_Flags_0.GetFFlagUpdateDeviceInputPlayerChanged,
        GetFFlagUseAudioInstanceAdded = l_Flags_0.GetFFlagUseAudioInstanceAdded,
        GetFFlagEnableVoiceSignal = l_Flags_0.GetFFlagEnableVoiceSignal
    },
    Requests = {
        GetShowAgeVerificationOverlay = l_Requests_0.GetShowAgeVerificationOverlay
    }
};


local v0 = require(script.Parent.Parent.Dev.JestConfigs);
return {
    displayName = "VoiceChatCore",
    setupFilesAfterEnv = {
        v0.setupFiles.LogHandler,
        v0.setupFiles.UIBloxInitializer,
        v0.setupFiles.createPromiseRejectionHandler()
    }
};


local l_Parent_0 = script.Parent.Parent;
local v1 = require(l_Parent_0.React);
local v2 = require(l_Parent_0.Roact);
local v3 = require(l_Parent_0.UIBlox);
local v4 = require(l_Parent_0.Cryo);
local l_Panel3D_0 = v3.Core.VR.Panel3D;
local v6 = require(script.Parent.RoundedCornersFrame);
local v7 = require(script.Parent.useVRFloorOffset);
local l_ROUNDED_CORNER_RADIUS_0 = require(script.Parent.Constants).ROUNDED_CORNER_RADIUS;
return function(v9)
    local v10 = nil;
    v10 = if not v9.roundedCornersFrameColor then v9[v2.Children] else {
        RoundedFrame = v2.createElement(v6, {
            cornerRadius = l_ROUNDED_CORNER_RADIUS_0 / (384 / (v9.virtualScreenSize.X / v9.partSize.X)),
            backgroundColor = v9.roundedCornersFrameColor,
            backgroundTransparency = 0
        }, v9[v2.Children])
    };
    local v11 = v7();
    return v1.createElement(l_Panel3D_0, v4.Dictionary.join(v9, {
        offset = CFrame.lookAt(Vector3.new(v9.offset.X, v9.offset.Y - v11.Y, v9.offset.Z), (Vector3.new(0, v9.offset.Y - v11.Y, 0)))
    }), v10);
end;


local v0 = require(script.Parent.Parent.SharedFlags);
local l_GetFFlagAdjustVRFloor_0 = v0.GetFFlagAdjustVRFloor;
local l_GetFIntMaquettesAppPanelDistanceMultiplierPercentage_0 = v0.GetFIntMaquettesAppPanelDistanceMultiplierPercentage;
local v3 = not l_GetFFlagAdjustVRFloor_0() and 4.8 or -0.2;
local v4 = l_GetFIntMaquettesAppPanelDistanceMultiplierPercentage_0() / 100;
return {
    MainScreenResolution = Vector2.new(1600, 900),
    MainScreenSize = Vector2.new(4.5, 2.7) * v4,
    MainScreenCurvature = 70,
    MainScreenPosition = Vector3.new(0, v3, -4 * v4),
    AppSystemBarPosition = Vector3.new(-2.35 * v4, v3, -3.15 * v4),
    AppSystemBarSize = Vector2.new(0.495, 2.7) * v4,
    AppSystemBarResolution = Vector2.new(64, 360),
    CentralOverlayPosition = Vector3.new(0, v3, -3.5 * v4),
    ToastSize = Vector2.new(2.5, 1.5),
    ToastPosition = Vector3.new(0, 3.3, -2.8),
    ToastResolution = Vector2.new(650, 382),
    ToastCurvature = 50,
    DefaultCameraPosition = Vector3.new(0, v3, 0),
    FTUX = {
        UniverseId = "4510252459",
        PlaceId = "12887241816"
    },
    ROUNDED_CORNER_RADIUS = 15,
    PositionLockedPanelName = "PositionLocked"
};


local l_Parent_0 = script.Parent.Parent;
local v1 = require(l_Parent_0.Cryo);
local v2 = require(l_Parent_0.React);
return function(v3)
    return v2.createElement("Frame", {
        BackgroundColor3 = v3.backgroundColor,
        BackgroundTransparency = v3.backgroundTransparency,
        Size = UDim2.new(1, 0, 1, 0)
    }, v1.Dictionary.join({
        Padding = v2.createElement("UIPadding", {
            PaddingRight = UDim.new(0, v3.cornerRadius),
            PaddingLeft = UDim.new(0, v3.cornerRadius),
            PaddingTop = UDim.new(0, v3.cornerRadius),
            PaddingBottom = UDim.new(0, v3.cornerRadius)
        }),
        Corner = v2.createElement("UICorner", {
            CornerRadius = UDim.new(0, v3.cornerRadius)
        })
    }, v3.children));
end;


local l_Parent_0 = script.Parent.Parent;
local v1 = require(l_Parent_0.React);
local v2 = require(l_Parent_0.Roact);
local v3 = require(l_Parent_0.UIBlox);
local v4 = require(l_Parent_0.SharedFlags);
local l_RoactAppPolicy_0 = require(l_Parent_0.UniversalAppPolicy).RoactAppPolicy;
local l_Panel3D_0 = v3.Core.VR.Panel3D;
local l_useIsFPSAtTarget_0 = require(l_Parent_0.SplashScreenManager).useIsFPSAtTarget;
local v8 = require(script.Parent.AppPanel3D);
local v9 = require(script.Parent.RoundedCornersFrame);
local l_Object_0 = require(l_Parent_0.LuauPolyfill).Object;
local l_GetFFlagEnableVRWrapperTransparency_0 = v4.GetFFlagEnableVRWrapperTransparency;
local l_GetFFlagAdjustVRFloor_0 = v4.GetFFlagAdjustVRFloor;
local l_ROUNDED_CORNER_RADIUS_0 = require(script.Parent.Constants).ROUNDED_CORNER_RADIUS;
local _ = nil;
return (l_RoactAppPolicy_0.connect(function(v15)
    return {
        use3DPanels = v15.getUse3DPanels()
    };
end)(not l_GetFFlagAdjustVRFloor_0() and function(v16)
    local v17 = l_Object_0.assign({}, v16.vrProps, {
        hidden = v16.vrProps.hidden or not l_useIsFPSAtTarget_0()
    });
    if not v16.use3DPanels then
        return v1.createElement(v16.component, v16.props, v16[v2.Children]);
    else
        local _ = nil;
        return v1.createElement(l_Panel3D_0, v17, if not v17.roundedCornersFrameColor then v16[v2.Children] else {
            RoundedFrame = v2.createElement(v9, {
                cornerRadius = l_ROUNDED_CORNER_RADIUS_0 / (384 / (v17.virtualScreenSize.X / v17.partSize.X)),
                backgroundColor = v17.roundedCornersFrameColor,
                backgroundTransparency = not l_GetFFlagEnableVRWrapperTransparency_0() and 0 or (v17.backgroundTransparency or 0)
            }, v16[v2.Children])
        });
    end;
end or function(v19)
    local v20 = l_Object_0.assign({}, v19.vrProps, {
        hidden = v19.vrProps.hidden or not l_useIsFPSAtTarget_0()
    });
    if not v19.use3DPanels then
        return v1.createElement(v19.component, v19.props, v19[v2.Children]);
    else
        return v1.createElement(v8, v20, v19[v2.Children]);
    end;
end));


return {
    VRReadyWrapper = require(script.VRReadyWrapper),
    VRConstants = require(script.Constants),
    RoundedCornersFrame = require(script.RoundedCornersFrame),
    useVRFloorOffset = require(script.useVRFloorOffset),
    AppPanel3D = require(script.AppPanel3D),
    ["jest.config"] = script["jest.config"]
};


local v0 = require(script.Parent.Parent.Dev.JestConfigs);
return {
    displayName = "VrCompatibility",
    setupFilesAfterEnv = {
        v0.setupFiles.LogHandler,
        v0.setupFiles.UIBloxInitializer,
        v0.setupFiles.createPromiseRejectionHandler()
    }
};


local l_VRService_0 = game:GetService("VRService");
local l_Parent_0 = script.Parent.Parent;
local v2 = require(l_Parent_0.React);
local l_useExternalEvent_0 = require(l_Parent_0.UIBlox).Core.Hooks.useExternalEvent;
local v4 = require(script.Parent.getVRFloorOffset);
return function()
    local v5, v6 = v2.useState(v4());
    l_useExternalEvent_0(l_VRService_0.UserCFrameChanged, function(v7)
        if v7 == Enum.UserCFrame.Floor then
            v6(v4());
        end;
    end);
    return v5;
end;


local l_Parent_0 = script.Parent.Parent;
local l_HttpService_0 = game:GetService("HttpService");
local l_UserInputService_0 = game:GetService("UserInputService");
local l_BrowserService_0 = game:GetService("BrowserService");
local l_MessageBus_0 = require(l_Parent_0.MessageBus).MessageBus;
local v5 = require(l_Parent_0.t);
local l_l_UserInputService_0_Platform_0 = l_UserInputService_0:GetPlatform();
local l_AsyncQueue_0 = require(l_Parent_0.AppCommonLib).AsyncQueue;
local l_GetFFlagEnableUnifiedWebViews_0 = require(l_Parent_0.SharedFlags).GetFFlagEnableUnifiedWebViews;
local _ = require(script.Parent.WebViewProtocolTypes);
game:DefineFastFlag("EnableWebViewProtocolForTestingOnly", false);
local function v11(v10)
    return l_HttpService_0:JSONEncode({
        command = "config",
        visible = v10.isVisible == true
    });
end;
local v12 = {
    OPEN_WINDOW_DESCRIPTOR = {
        mid = l_MessageBus_0.getMessageId("WebView", "openWindow"),
        validateParams = v5.strictInterface({
            url = v5.string,
            title = v5.optional(v5.string),
            isVisible = v5.optional(v5.boolean),
            searchParams = v5.optional(v5.strictInterface({
                searchType = v5.string
            })),
            transitionAnimation = v5.optional(v5.string),
            windowType = if not l_GetFFlagEnableUnifiedWebViews_0() then v5.string else v5.optional(v5.string),
            visible = if not l_GetFFlagEnableUnifiedWebViews_0() then nil else v5.optional(v5.boolean)
        })
    },
    MUTATE_WINDOW_DESCRIPTOR = {
        mid = l_MessageBus_0.getMessageId("WebView", "mutateWindow"),
        validateParams = v5.strictInterface({
            url = v5.optional(v5.string),
            title = v5.optional(v5.string),
            isVisible = v5.optional(v5.boolean),
            searchParams = v5.optional(v5.strictInterface({
                searchType = v5.string
            })),
            transitionAnimation = v5.optional(v5.string),
            windowType = v5.optional(v5.string)
        })
    },
    CLOSE_WINDOW_DESCRIPTOR = {
        mid = l_MessageBus_0.getMessageId("WebView", "closeWindow"),
        validateParams = v5.table
    },
    HANDLE_WINDOW_CLOSE_DESCRIPTOR = {
        mid = l_MessageBus_0.getMessageId("WebView", "handleWindowClose"),
        validateParams = v5.table
    }
};
v12.__index = v12;
v12.new = function(v13, v14)
    local v15 = v13 or l_MessageBus_0;
    local v16 = {
        messageBus = v15,
        browserService = if not l_GetFFlagEnableUnifiedWebViews_0() then nil else v14 or l_BrowserService_0,
        subscriber = v15.Subscriber.new(),
        queue = if not l_GetFFlagEnableUnifiedWebViews_0() then nil else l_AsyncQueue_0.new()
    };
    setmetatable(v16, v12);
    return v16;
end;
v12.openWindow = function(v17, v18)
    assert(v17.isEnabled());
    if l_GetFFlagEnableUnifiedWebViews_0() then
        if v17.shouldForceVisibleOnOpen() then
            v18.isVisible = true;
        end;
        if v17.shouldRenameIsVisibleParam() then
            v18.visible = v18.isVisible;
        end;
    end;
    v17.messageBus.publish(v17.OPEN_WINDOW_DESCRIPTOR, v18);
end;
v12.mutateWindow = function(v19, v20)
    assert(v19.isEnabled());
    local v21 = true;
    if not (not l_GetFFlagEnableUnifiedWebViews_0() or not v19.shouldForceBrowserServiceOnMutate()) then
        v21 = false;
    end;
    if not v21 then
        v19.browserService:SendCommand(v11(v20));
        return ;
    else
        v19.messageBus.publish(v19.MUTATE_WINDOW_DESCRIPTOR, v20);
        return ;
    end;
end;
v12.closeWindow = function(v22)
    assert(v22.isEnabled());
    v22.messageBus.publish(v22.CLOSE_WINDOW_DESCRIPTOR, {});
end;
v12.listenForWindowClose = function(v23, v24)
    assert(v23.isEnabled());
    v23.subscriber:subscribe(v23.HANDLE_WINDOW_CLOSE_DESCRIPTOR, function()
        v24();
    end, false);
end;
v12.stopListeningForWindowClose = function(v25)
    assert(v25.isEnabled());
    v25.subscriber:unsubscribe(v25.HANDLE_WINDOW_CLOSE_DESCRIPTOR);
end;
v12.listenForJavaScript = function(v26, v27)
    v26.javaScriptConnection = v26.browserService.JavaScriptCallback:Connect(v27);
end;
v12.stopListeningForJavaScript = function(v28)
    if v28.javaScriptConnection ~= nil then
        v28.javaScriptConnection:Disconnect();
        v28.javaScriptConnection = nil;
    end;
end;
v12.stopListening = function(v29)
    assert(v29.isEnabled());
    v29.subscriber:unsubscribeFromAllMessages();
end;
v12.request = function(v30, v31)
    return v30.queue:request(v31);
end;
v12.isEnabled = function()
    return game:GetEngineFeature("WebViewProtocol") or game:GetFastFlag("EnableWebViewProtocolForTestingOnly");
end;
v12.shouldForceVisibleOnOpen = function()
    local v32 = true;
    if l_l_UserInputService_0_Platform_0 ~= Enum.Platform.Windows then
        v32 = l_l_UserInputService_0_Platform_0 == Enum.Platform.OSX;
    end;
    return v32;
end;
v12.shouldRenameIsVisibleParam = function()
    return l_l_UserInputService_0_Platform_0 == Enum.Platform.UWP;
end;
v12.shouldForceBrowserServiceOnMutate = function()
    return l_l_UserInputService_0_Platform_0 == Enum.Platform.UWP;
end;
v12.default = v12.new();
return v12;


local _ = require(script.WebViewProtocolTypes);
return require(script.WebViewProtocol);


export type PNG = {
	width: number,
	height: number,
	pixels: buffer,
	readPixel: (x: number, y: number) -> (number, number, number, number),
}

export type Chunk = {
	type: string,
	offset: number,
	length: number,
}

export type IHDRChunk = {
	width: number,
	height: number,
	bitDepth: number,
	colorType: number,
	interlaced: boolean,
}

type PaletteColor = {
	r: number,
	g: number,
	b: number,
	a: number,
}

export type PLTEChunk = {
	colors: { PaletteColor },
}

export type tRNSChunk = {
	gray: number,
	red: number,
	green: number,
	blue: number,
}

return {}


local Types = require("../Types")

local COLOR_TYPE_BIT_DEPTH = {
	[0] = { 1, 2, 4, 8, 16 },
	[2] = { 8, 16 },
	[3] = { 1, 2, 4, 8 },
	[4] = { 8, 16 },
	[6] = { 8, 16 },
}

local function read(buf: buffer, chunk: Types.Chunk): Types.IHDRChunk
	assert(chunk.length == 13, "IHDR data must be 13 bytes")

	local offset = chunk.offset

	local width = bit32.byteswap(buffer.readu32(buf, offset))
	local height = bit32.byteswap(buffer.readu32(buf, offset + 4))
	local bitDepth = buffer.readu8(buf, offset + 8)
	local colorType = buffer.readu8(buf, offset + 9)
	local compression = buffer.readu8(buf, offset + 10)
	local filter = buffer.readu8(buf, offset + 11)
	local interlace = buffer.readu8(buf, offset + 12)

	assert(width > 0 and width <= 2 ^ 31 and height > 0 and height <= 2 ^ 31, "invalid dimensions")
	assert(compression == 0, "invalid compression method")
	assert(filter == 0, "invalid filter method")
	assert(interlace == 0 or interlace == 1, "invalid interlace method")

	local allowedBitDepth = COLOR_TYPE_BIT_DEPTH[colorType]
	assert(allowedBitDepth ~= nil, "invalid color type")
	assert(table.find(allowedBitDepth, bitDepth) ~= nil, "invalid bit depth")

	return {
		width = width,
		height = height,
		bitDepth = bitDepth,
		colorType = colorType,
		interlaced = interlace == 1,
	}
end

return read


local Types = require("../Types")

local function read(buf: buffer, chunk: Types.Chunk, header: Types.IHDRChunk): Types.PLTEChunk
	assert(chunk.length % 3 == 0, "malformed PLTE chunk")

	local count = chunk.length / 3
	assert(count > 0, "no entries in PLTE")
	assert(count <= 256, "too many entries in PLTE")
	assert(count <= 2 ^ header.bitDepth, "too many entries in PLTE for bit depth")

	local colors = table.create(count)
	local offset = chunk.offset

	for i = 1, count do
		colors[i] = {
			r = buffer.readu8(buf, offset),
			g = buffer.readu8(buf, offset + 1),
			b = buffer.readu8(buf, offset + 2),
			a = 255,
		}
		offset += 3
	end

	return {
		colors = colors,
	}
end

return read


return {
	IHDR = require("@self/IHDR"),
	PLTE = require("@self/PLTE"),
	tRNS = require("@self/tRNS"),
}


local Types = require("../Types")

local function readU16(buf: buffer, offset: number, depth: number)
	return bit32.extract(
		bit32.bor(bit32.lshift(buffer.readu8(buf, offset), 8), buffer.readu8(buf, offset + 1)),
		0,
		depth
	)
end

local function read(
	buf: buffer,
	chunk: Types.Chunk,
	header: Types.IHDRChunk,
	palette: Types.PLTEChunk?
): Types.tRNSChunk
	local gray = -1
	local red = -1
	local green = -1
	local blue = -1

	if header.colorType == 0 then
		assert(chunk.length == 2, "invalid tRNS length for color type")
		gray = readU16(buf, chunk.offset, header.bitDepth)
	elseif header.colorType == 2 then
		assert(chunk.length == 6, "invalid tRNS length for color type")
		red = readU16(buf, chunk.offset, header.bitDepth)
		green = readU16(buf, chunk.offset + 2, header.bitDepth)
		blue = readU16(buf, chunk.offset + 4, header.bitDepth)
	else
		local count = chunk.length -- 1 byte per transparency entry
		assert(palette, "tRNS requires PLTE for color type")
		assert(count <= #palette.colors, "tRNS specified too many PLTE alphas")
		for i = 1, count do
			palette.colors[i].a = buffer.readu8(buf, chunk.offset + i - 1)
		end
	end

	return {
		gray = gray,
		red = red,
		green = green,
		blue = blue,
	}
end

return read


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

local function compare(a, b)
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end

	for key, value in a do
		if b[key] ~= value then
			return false
		end
	end

	for key, value in b do
		if a[key] ~= value then
			return false
		end
	end

	return true
end

local function equals<K, V>(...: { [K]: V })
	if equalObjects(...) then
		return true
	end

	local argCount = select("#", ...)
	local firstObject = select(1, ...)

	for i = 2, argCount do
		local object = select(i, ...)

		if not compare(firstObject, object) then
			return false
		end
	end

	return true
end

return equals


--!strict
--[=[
	Returns a Dictionary of only entries for which the `predicate` function returns true.
]=]

local function filter<K, V>(dictionary: { [K]: V }, predicate: ((V, K) -> boolean)): { [K]: V }
	local new = {}

	for k, v in dictionary do
		if predicate(v, k) then
			new[k] = v
		end
	end

	return new
end

return filter


--[[
	Defines utilities for working with 'dictionary-like' tables.

	Dictionaries can be indexed by any value, but don't have the ordering
	expectations that lists have.
]]

return {
	equals = require(script.equals),
	filter = require(script.filter),
	join = require(script.join),
	keys = require(script.keys),
	map = require(script.map),
	omit = require(script.omit),
	union = require(script.union),
	values = require(script.values),
}


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

--[[
	Combine a number of dictionary-like tables into a new table.

	Keys specified in later tables will overwrite keys in previous tables.

	Use `Cryo.None` as a value to remove a key. This is necessary because
	Lua does not distinguish between a value not being present in a table and a
	value being `nil`.
]]
local function join(...)
	local argCount = select("#", ...)
	if argCount == 0 then
		return {}
	elseif argCount == 1 then
		local new = table.clone(...)
		setmetatable(new, nil) -- We don't want cloned metatables
		return new
	end

	local new = table.clone(select(1, ...))
	setmetatable(new, nil) -- We don't want cloned metatables

	for i = 2, argCount do
		local source = select(i, ...)

		for key, value in pairs(source) do
			if value == None then
				new[key] = nil
			else
				new[key] = value
			end
		end
	end

	return new
end

return join


--[[
	Returns a list of the keys from the given dictionary.
]]
local function keys(dictionary)
	local new = {}
	local index = 1

	for key in pairs(dictionary) do
		new[index] = key
		index = index + 1
	end

	return new
end

return keys


--!strict
--[=[
	Returns a new Dictionary with keys and values passed through a mapper function.

	Returning a second value in the mapper function will reassign the key.
]=]

local function map<K, V, K2, V2>(collection: { [K]: V }, mapper: (V, K) -> (V2, K2?)): { [K2]: V2 }
	local new = {}

	for key, value in collection do
		local mappedValue, mappedKey = mapper(value, key)
		local newKey = if mappedKey == nil then key else mappedKey

		if mappedValue ~= nil then
			new[newKey] = mappedValue
		end
	end

	return new :: { [K2]: V2 }
end

return map


--!strict
--[=[
	Returns a Dictionary which excludes the given `keys`.
]=]

local function omit<K, V>(dictionary: { [K]: V }, ...: K): { [K]: V }
	local new = table.clone(dictionary)
	for _, key in { ... } do
		new[key] = nil
	end
	return new
end

return omit


--!strict
local function union<T, U>(dictionary1: T, dictionary2: U): T & U
	local new: any = table.clone(dictionary1 :: any)

	for key, value in dictionary2 :: any do
		new[key] = value
	end

	return new
end

return union


--[[
	Returns a list of the values of the given dictionary.
]]

local function values(dictionary)
	local new = {}
	local index = 1

	for _, value in pairs(dictionary) do
		new[index] = value
		index = index + 1
	end

	return new
end

return values


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

local function compare(a, b)
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end

	if #a ~= #b then
		return false
	end

	for i = 1, #a do
		if a[i] ~= b[i] then
			return false
		end
	end

	return true
end

local function equals<T>(...: { T })
	if equalObjects(...) then
		return true
	end

	local argCount = select("#", ...)
	local firstObject = select(1, ...)

	for i = 2, argCount do
		local object = select(i, ...)

		if not compare(firstObject, object) then
			return false
		end
	end

	return true
end

return equals


--[[
	Create a copy of a list with only values for which `callback` returns true.
	Calls the callback with (value, index).
]]
local function filter(list, callback)
	local new = {}
	local index = 1

	for i = 1, #list do
		local value = list[i]
		if callback(value, i) then
			new[index] = value
			index = index + 1
		end
	end

	return new
end

return filter


--[[
	Create a copy of a list doing a combination filter and map.

	If callback returns nil for any item, it is considered filtered from the
	list. Any other value is considered the result of the 'map' operation.
]]
local function filterMap(list, callback)
	local new = {}
	local index = 1

	for i = 1, #list do
		local result = callback(list[i], i)

		if result ~= nil then
			new[index] = result
			index = index + 1
		end
	end

	return new
end

return filterMap


--[[
	Returns the index of the first value found or nil if not found.
]]

local function find(list, value)
	for i = 1, #list do
		if list[i] == value then
			return i
		end
	end
	return nil
end

return find


--[[
	Returns the index of the first value for which predicate(value, index) is truthy, or nil if not found.
]]

local function findWhere(list, predicate)
	for i = 1, #list do
		if predicate(list[i], i) then
			return i
		end
	end
	return nil
end

return findWhere


--[[
	Performs a left-fold of the list with the given initial value and callback.
]]
local function foldLeft(list, callback, initialValue)
	local accum = initialValue

	for i = 1, #list do
		accum = callback(accum, list[i], i)
	end

	return accum
end

return foldLeft


--[[
	Performs a right-fold of the list with the given initial value and callback.
]]
local function foldRight(list, callback, initialValue)
	local accum = initialValue

	for i = #list, 1, -1 do
		accum = callback(accum, list[i], i)
	end

	return accum
end

return foldRight


--[[
	Returns a new list containing only the elements within the given range.
]]

local function getRange(list, startIndex, endIndex)
	assert(startIndex <= endIndex, "startIndex must be less than or equal to endIndex")

	local new = {}
	local index = 1

	for i = math.max(1, startIndex), math.min(#list, endIndex) do
		new[index] = list[i]
		index = index + 1
	end

	return new
end

return getRange


--[[
	Defines utilities for working with 'list-like' tables.
]]

return {
	equals = require(script.equals),
	filter = require(script.filter),
	filterMap = require(script.filterMap),
	find = require(script.find),
	findWhere = require(script.findWhere),
	foldLeft = require(script.foldLeft),
	foldRight = require(script.foldRight),
	getRange = require(script.getRange),
	join = require(script.join),
	map = require(script.map),
	removeIndex = require(script.removeIndex),
	removeRange = require(script.removeRange),
	removeValue = require(script.removeValue),
	replaceIndex = require(script.replaceIndex),
	reverse = require(script.reverse),
	sort = require(script.sort),
	toSet = require(script.toSet),
}


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

--[[
	Joins any number of lists together into a new list
]]
local function join(...)
	local new = {}

	for listKey = 1, select("#", ...) do
		local list = select(listKey, ...)
		local len = #new

		for itemKey = 1, #list do
			if list[itemKey] == None then
				len = len - 1
			else
				new[len + itemKey] = list[itemKey]
			end
		end
	end

	return new
end

return join


--[[
	Create a copy of a list where each value is transformed by `callback`
]]
local function map(list, callback)
	local new = {}

	for i = 1, #list do
		new[i] = callback(list[i], i)
	end

	return new
end

return map


--[[
	Remove the element at the given index.
]]
local function removeIndex(list, index)
	local new = {}
	local removed = 0

	for i = 1, #list do
		if i == index then
			removed = 1
		else
			new[i - removed] = list[i]
		end
	end

	return new
end

return removeIndex


--[[
	Remove the range from the list starting from the index.
]]
local function removeRange(list, startIndex, endIndex)
	assert(startIndex <= endIndex, "startIndex must be less than or equal to endIndex")

	local new = {}
	local index = 1

	for i = 1, math.min(#list, startIndex - 1) do
		new[index] = list[i]
		index = index + 1
	end

	for i = endIndex + 1, #list do
		new[index] = list[i]
		index = index + 1
	end

	return new
end

return removeRange


--[[
	Creates a new list that has no occurrences of the given value.
]]
local function removeValue(list, value)
	local new = {}
	local index = 1

	for i = 1, #list do
		if list[i] ~= value then
			new[index] = list[i]
			index = index + 1
		end
	end

	return new
end

return removeValue


--[[
	Returns a new list with the new value replaced at the given index.
]]

local function replaceIndex(list, index, value)
	local new = {}
	local len = #list

	assert(index <= len, "index must be less or equal than the list length")

	for i = 1, len do
		if i == index then
			new[i] = value
		else
			new[i] = list[i]
		end
	end

	return new
end

return replaceIndex


--[[
	Returns a new list with the reversed order of the given list
]]

local function reverse(list)
	local new = {}
	local len = #list
	local top = len + 1

	for i = 1, len do
		new[i] = list[top - i]
	end

	return new
end

return reverse


--[[
	Returns a new list, ordered with the given sort callback.
	If no callback is given, the default table.sort will be used.
]]

local function sort(list, callback)
	local new = {}

	for i = 1, #list do
		new[i] = list[i]
	end

	table.sort(new, callback)

	return new
end

return sort


--[[
	Represents a value that is intentionally present, but should be interpreted
	as `nil`.

	Cryo.None is used by included utilities to make removing values more
	ergonomic.
]]

local None = newproxy(true)

getmetatable(None).__tostring = function()
	return "Cryo.None"
end

return None


--!strict
local function equalObjects(...)
	local firstObject = select(1, ...)

	for i = 2, select("#", ...) do
		if firstObject ~= select(i, ...) then
			return false
		end
	end

	return true
end

return equalObjects


--!strict
local equalObjects = require(script.Parent.equalObjects)

local function compareDeep(a, b)
	if type(a) ~= "table" or type(b) ~= "table" then
		return a == b
	end

	for key, value in a do
		if not compareDeep(b[key], value) then
			return false
		end
	end

	for key, value in b do
		if not compareDeep(a[key], value) then
			return false
		end
	end

	return true
end

local function equalsDeep(...)
	if equalObjects(...) then
		return true
	end

	local argCount = select("#", ...)
	local firstObject = select(1, ...)

	for i = 2, argCount do
		local object = select(i, ...)

		if not compareDeep(firstObject, object) then
			return false
		end
	end

	return true
end

return equalsDeep


return {
	Dictionary = require(script.Dictionary),
	List = require(script.List),
	isEmpty = require(script.isEmpty),
	None = require(script.None),
}


local function isEmpty(object)
	return next(object) == nil
end

return isEmpty


-- Lest chokes on .server.lua files so we pass the `spec` module to it and mount
-- this file in our project so we can run tests in Studio.
require(script.spec)


local ReplicatedStorage = game:GetService("ReplicatedStorage")

local TestEZ = require(ReplicatedStorage.Packages.Dev.TestEZ)

local results = TestEZ.TestBootstrap:run({
	ReplicatedStorage.Packages.Cryo,
})

local success, ProcessService = pcall(game.GetService, game, "ProcessService")
if success and ProcessService then
	ProcessService:ExitAsync(results.failureCount == 0 and 0 or 1)
end

return nil


local Root = script:FindFirstAncestor("ResourcesReact")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it
local describe = JestGlobals.describe
local ReactRoblox = require(Packages.Dev.ReactRoblox)

local React = require(Packages.React)

local Resources = require(Packages.Resources)
local createResource = Resources.createResource

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

describe("useResource", function()
	it("should create and dispose resource with component lifecycle", function()
		local constructCount = 0
		local disposeCount = 0

		local function createTestResource()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
			end)
		end

		local function TestComponent()
			useResource(createTestResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0)

		ReactRoblox.act(function()
			root:unmount()
		end)

		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(1)

		container:Destroy()
	end)

	it("should pass arguments to resource", function()
		local capturedArgs = nil

		local function TestComponent()
			local function createTestResource()
				return createResource(function()
					capturedArgs = { 1, 2, 3 }
				end)
			end

			useResource(createTestResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(capturedArgs).toEqual({ 1, 2, 3 })

		ReactRoblox.act(function()
			root:unmount()
		end)

		container:Destroy()
	end)

	it("should not recreate resource when dependencies stay the same", function()
		local constructCount = 0
		local disposeCount = 0

		local function createTestResource()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
			end)
		end

		local function TestComponent()
			useResource(createTestResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0)

		ReactRoblox.act(function()
			root:unmount()
		end)

		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(1)

		container:Destroy()
	end)

	it("should handle resources with no arguments", function()
		local constructCount = 0

		local function createTestResource()
			return createResource(function()
				constructCount += 1
			end)
		end

		local function TestComponent()
			useResource(createTestResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(constructCount).toBe(1)

		ReactRoblox.act(function()
			root:unmount()
		end)

		container:Destroy()
	end)

	it("should handle nested resources", function()
		local innerConstructCount = 0
		local innerDisposeCount = 0
		local outerConstructCount = 0
		local outerDisposeCount = 0

		local function createInnerResource()
			return createResource(function(own)
				innerConstructCount += 1
				own(function()
					innerDisposeCount += 1
				end)
			end)
		end

		local function createOuterResource()
			return createResource(function(own)
				outerConstructCount += 1
				own(createInnerResource())
				own(function()
					outerDisposeCount += 1
				end)
			end)
		end

		local function TestComponent()
			useResource(createOuterResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(innerConstructCount).toBe(1)
		expect(outerConstructCount).toBe(1)
		expect(innerDisposeCount).toBe(0)
		expect(outerDisposeCount).toBe(0)

		ReactRoblox.act(function()
			root:unmount()
		end)

		expect(innerConstructCount).toBe(1)
		expect(outerConstructCount).toBe(1)
		expect(innerDisposeCount).toBe(1)
		expect(outerDisposeCount).toBe(1)

		container:Destroy()
	end)

	it("should handle multiple components using same resource", function()
		local constructCount = 0
		local disposeCount = 0

		local function createTestResource()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
			end)
		end

		local function TestComponent()
			useResource(createTestResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement("Folder", {}, {
				Child1 = React.createElement(TestComponent),
				Child2 = React.createElement(TestComponent),
				Child3 = React.createElement(TestComponent),
			}))
		end)

		expect(constructCount).toBe(3)
		expect(disposeCount).toBe(0)

		ReactRoblox.act(function()
			root:unmount()
		end)

		expect(constructCount).toBe(3)
		expect(disposeCount).toBe(3)

		container:Destroy()
	end)

	it("should handle resource changes with different resource objects", function()
		local resource1ConstructCount = 0
		local resource1DisposeCount = 0
		local resource2ConstructCount = 0
		local resource2DisposeCount = 0

		local function createResource1()
			return createResource(function(own)
				resource1ConstructCount += 1
				own(function()
					resource1DisposeCount += 1
				end)
			end)
		end

		local function createResource2()
			return createResource(function(own)
				resource2ConstructCount += 1
				own(function()
					resource2DisposeCount += 1
				end)
			end)
		end

		local function TestComponent(props)
			useResource(props.resourceFactory)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent, { resourceFactory = createResource1 }))
		end)

		expect(resource1ConstructCount).toBe(1)
		expect(resource1DisposeCount).toBe(0)
		expect(resource2ConstructCount).toBe(0)
		expect(resource2DisposeCount).toBe(0)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent, { resourceFactory = createResource2 }))
		end)

		expect(resource1ConstructCount).toBe(1)
		expect(resource1DisposeCount).toBe(1)
		expect(resource2ConstructCount).toBe(1)
		expect(resource2DisposeCount).toBe(0)

		ReactRoblox.act(function()
			root:unmount()
		end)

		expect(resource1ConstructCount).toBe(1)
		expect(resource1DisposeCount).toBe(1)
		expect(resource2ConstructCount).toBe(1)
		expect(resource2DisposeCount).toBe(1)

		container:Destroy()
	end)

	it("should work with nil arguments", function()
		local constructCount = 0

		local function createTestResource()
			return createResource(function()
				constructCount += 1
			end)
		end

		local function TestComponent()
			useResource(createTestResource)
			return nil
		end

		local container = Instance.new("Folder")
		local root = ReactRoblox.createRoot(container)

		ReactRoblox.act(function()
			root:render(React.createElement(TestComponent))
		end)

		expect(constructCount).toBe(1)

		ReactRoblox.act(function()
			root:unmount()
		end)

		container:Destroy()
	end)
end)


local Packages = script.Parent.Parent
local React = require(Packages.React)
local Resources = require(Packages.Resources)
type resource<A..., R...> = Resources.resource<A..., R...>

local function useResource<T>(resource: resource<(), (T)>): T?
	local value, setValue = React.useState(function(): T?
		return nil
	end)

	React.useLayoutEffect(function()
		local dispose, result = resource()
		setValue(result)
		return function()
			setValue(nil)
			dispose()
		end
	end, { resource })

	return value
end

return useResource


local Root = script:FindFirstAncestor("Resources")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it
local describe = JestGlobals.describe

local createResource = require(script.Parent.Parent.createResource)
local createCachedResource = require(script.Parent.Parent.createCachedResource)

describe("createCachedResource", function()
	it("should create a cached resource that can be used multiple times", function()
		local constructCount = 0
		local disposeCount = 0

		local cachedResource = createCachedResource(function()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
				return "value"
			end)
		end)

		expect(constructCount).toBe(0)
		expect(disposeCount).toBe(0)

		local dispose1, value1 = cachedResource()
		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0)
		expect(value1).toBe("value")

		local dispose2, value2 = cachedResource()
		expect(constructCount).toBe(1) -- Should not construct again
		expect(disposeCount).toBe(0)
		expect(value2).toBe("value")

		local dispose3, value3 = cachedResource()
		expect(constructCount).toBe(1) -- Should not construct again
		expect(disposeCount).toBe(0)
		expect(value3).toBe("value")

		dispose1()
		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0) -- Should not dispose yet

		dispose2()
		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(0) -- Should not dispose yet

		dispose3()
		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(1) -- Now it should dispose
	end)

	it("should reconstruct resource after all references are disposed", function()
		local constructCount = 0
		local disposeCount = 0

		local cachedResource = createCachedResource(function()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
				return "value"
			end)
		end)

		local dispose1, value1 = cachedResource()
		expect(constructCount).toBe(1)
		expect(value1).toBe("value")

		dispose1()
		expect(constructCount).toBe(1)
		expect(disposeCount).toBe(1)

		-- Use again after fully disposing
		local dispose2, value2 = cachedResource()
		expect(constructCount).toBe(2) -- Should reconstruct
		expect(disposeCount).toBe(1)
		expect(value2).toBe("value")

		dispose2()
		expect(constructCount).toBe(2)
		expect(disposeCount).toBe(2)
	end)

	it("should handle calling dispose multiple times for same reference", function()
		local constructCount = 0
		local disposeCount = 0

		local cachedResource = createCachedResource(function()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
				return "value"
			end)
		end)

		local dispose1, _ = cachedResource()
		local dispose2, _ = cachedResource()

		expect(constructCount).toBe(1)

		dispose1()
		dispose1()
		dispose1()

		expect(disposeCount).toBe(0) -- Still has one reference

		dispose2()
		expect(disposeCount).toBe(1) -- Now fully disposed
	end)

	it("should dispose when error occurs during construction", function()
		local cachedResource = createCachedResource(function()
			error("construction failed")
		end)

		expect(function()
			cachedResource()
		end).toThrow("construction failed")

		-- After error, references should be 0
		-- A second call should attempt construction again
		expect(function()
			cachedResource()
		end).toThrow("construction failed")
	end)

	it("should maintain consistent value across all references", function()
		local counter = 0

		local cachedResource = createCachedResource(function()
			return createResource(function(_)
				counter += 1
				return counter
			end)
		end)

		local dispose1, value1 = cachedResource()
		local dispose2, value2 = cachedResource()
		local dispose3, value3 = cachedResource()

		expect(value1).toBe(1)
		expect(value2).toBe(1)
		expect(value3).toBe(1)

		dispose1()
		dispose2()
		dispose3()

		-- After reconstruction, counter should be 2
		local dispose4, value4 = cachedResource()
		expect(value4).toBe(2)
		dispose4()
	end)

	it("should support complex resources with nested children", function()
		local childConstructCount = 0
		local childDisposeCount = 0
		local parentConstructCount = 0
		local parentDisposeCount = 0

		local function createChildResource()
			return createResource(function(own)
				childConstructCount += 1
				own(function()
					childDisposeCount += 1
				end)
				return "child"
			end)
		end

		local function createParentResource()
			return createResource(function(own)
				parentConstructCount += 1
				local childValue = own(createChildResource())
				own(function()
					parentDisposeCount += 1
				end)
				return "parent-" .. childValue
			end)
		end

		local cachedResource = createCachedResource(createParentResource)

		local dispose1, value1 = cachedResource()
		expect(value1).toBe("parent-child")
		expect(childConstructCount).toBe(1)
		expect(parentConstructCount).toBe(1)

		local dispose2, value2 = cachedResource()
		expect(value2).toBe("parent-child")
		expect(childConstructCount).toBe(1) -- Should not reconstruct
		expect(parentConstructCount).toBe(1)

		dispose1()
		expect(childDisposeCount).toBe(0)
		expect(parentDisposeCount).toBe(0)

		dispose2()
		expect(childDisposeCount).toBe(1) -- Should dispose child
		expect(parentDisposeCount).toBe(1) -- Should dispose parent
	end)

	it("should handle zero-argument resources", function()
		local constructCount = 0

		local cachedResource = createCachedResource(function()
			return createResource(function(_)
				constructCount += 1
				return "singleton"
			end)
		end)

		local dispose1, value1 = cachedResource()
		local dispose2, value2 = cachedResource()

		expect(constructCount).toBe(1)
		expect(value1).toBe("singleton")
		expect(value2).toBe("singleton")

		dispose1()
		dispose2()

		expect(constructCount).toBe(1)
	end)

	it("should properly clean up value on dispose", function()
		local cachedResource = createCachedResource(function()
			return createResource(function(_)
				return { data = "sensitive" }
			end)
		end)

		local dispose1, value1 = cachedResource()
		expect(value1.data).toBe("sensitive")

		local dispose2, value2 = cachedResource()
		expect(value2.data).toBe("sensitive")

		dispose1()
		dispose2()

		-- After full disposal, getting the resource again should create a new value
		local dispose3, value3 = cachedResource()
		expect(value3.data).toBe("sensitive")
		expect(value3).never.toBe(value1) -- Should be a new table

		dispose3()
	end)

	it("should handle interleaved construction and disposal", function()
		local constructCount = 0
		local disposeCount = 0

		local cachedResource = createCachedResource(function()
			return createResource(function(own)
				constructCount += 1
				own(function()
					disposeCount += 1
				end)
				return constructCount
			end)
		end)

		local dispose1, value1 = cachedResource()
		expect(value1).toBe(1)
		expect(constructCount).toBe(1)

		local dispose2, value2 = cachedResource()
		expect(value2).toBe(1)
		expect(constructCount).toBe(1)

		dispose1()
		expect(disposeCount).toBe(0)

		local dispose3, value3 = cachedResource()
		expect(value3).toBe(1)
		expect(constructCount).toBe(1)

		dispose2()
		expect(disposeCount).toBe(0)

		dispose3()
		expect(disposeCount).toBe(1)

		-- Now fully disposed, should reconstruct
		local dispose4, value4 = cachedResource()
		expect(value4).toBe(2)
		expect(constructCount).toBe(2)

		dispose4()
		expect(disposeCount).toBe(2)
	end)
end)


local Root = script:FindFirstAncestor("Resources")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it
local describe = JestGlobals.describe

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

describe("createResource", function()
	it("should allow resource to be called directly", function()
		local called = false
		local dispose, value = createResource(function(_)
			called = true
			return "test-value"
		end)

		expect(called).toBe(true)
		expect(value).toBe("test-value")

		dispose()
	end)

	it("should support resources with no arguments", function()
		local dispose, value = createResource(function(_)
			return 42
		end)

		expect(value).toBe(42)

		dispose()
	end)

	it("should support resources with multiple return values", function()
		local dispose, a, b, c = createResource(function(_)
			return 1, 2, 3
		end)

		expect(a).toBe(1)
		expect(b).toBe(2)
		expect(c).toBe(3)

		dispose()
	end)

	it("should call dispose function when root is disposed", function()
		local disposed = false
		local dispose, value = createResource(function(own)
			own(function()
				disposed = true
			end)
			return "value"
		end)

		expect(disposed).toBe(false)
		expect(value).toBe("value")

		dispose()

		expect(disposed).toBe(true)
	end)

	it("should support nested resource usage", function()
		local dispose, value = createResource(function(own)
			local innerValue = own(createResource(function(_)
				return "inner"
			end))
			return "outer-" .. innerValue
		end)

		expect(value).toBe("outer-inner")

		dispose()
	end)

	it("should dispose in reverse order", function()
		local disposeOrder: { string | number } = {}

		local dispose = createResource(function(own)
			own(createResource(function(own2)
				own2(function()
					table.insert(disposeOrder, 1)
				end)
				return "first"
			end))
			own(createResource(function(own2)
				own2(function()
					table.insert(disposeOrder, 2)
				end)
				return "second"
			end))
			own(createResource(function(own2)
				own2(function()
					table.insert(disposeOrder, 3)
				end)
				return "third"
			end))
			own(function()
				table.insert(disposeOrder, "parent")
			end)
		end)

		expect(#disposeOrder).toBe(0)

		dispose()

		expect(disposeOrder).toEqual({ "parent" :: string | number, 3, 2, 1 })
	end)

	it("should handle string errors during disposal", function()
		local dispose = createResource(function(own)
			own(function()
				error("table error")
			end)
		end)

		expect(function()
			dispose()
		end).toThrow("table error")
	end)

	it("should handle non-string errors during disposal", function()
		local tableError = { message = "table error" }
		local dispose = createResource(function(own)
			own(function()
				error(tableError)
			end)
		end)

		expect(function()
			dispose()
		end).toThrow(tostring(tableError))
	end)

	it("should handle multiple non-string errors during disposal", function()
		local tableError = { message = "table error" }
		local functionError = function() end
		local dispose = createResource(function(own)
			own(function()
				error(functionError)
			end)
			own(function()
				error(tableError)
			end)
			own(function()
				error(true)
			end)
		end)

		expect(function()
			dispose()
		end).toThrow(`{tostring(true)}\n{tostring(tableError)}\n{tostring(functionError)}`)
	end)
end)


local types = require(script.Parent.types)
type dispose = types.dispose
type resource<A..., R...> = types.resource<A..., R...>

local function once(dispose: dispose): dispose
	local called = false

	local function notAgain()
		if not called then
			called = true
			dispose()
		end
	end

	return notAgain
end

local function createCachedResource<T>(resource: resource<(), (T)>): resource<(), (T)>
	local value: T? = nil
	local dispose: dispose? = nil
	local references = 0

	local function disposeResource()
		references -= 1
		if references == 0 then
			value = nil
			if dispose ~= nil then
				local toDispose = dispose
				dispose = nil
				toDispose()
			end
		end
	end

	local function constructResource()
		dispose, value = resource()
	end

	local function handleError(ok: boolean, err: any)
		if not ok then
			disposeResource()
			error(err)
		end
	end

	local function cachedResource()
		references += 1
		if references == 1 then
			handleError(pcall(constructResource))
		end

		return once(disposeResource), value :: T
	end

	return cachedResource
end

return createCachedResource


local types = require(script.Parent.types)
type own = types.own
type dispose = types.dispose

local function disposeChildren(children: { dispose })
	local errors: { string } = {}

	for i = #children, 1, -1 do
		local child = children[i]
		local ok, err: any = pcall(child)
		if not ok then
			table.insert(errors, tostring(err))
		end
	end

	if #errors > 0 then
		error(table.concat(errors, "\n"))
	end
end

local function createResource<T...>(resourceFn: (own) -> T...): (dispose, T...)
	local isDisposed = false

	local children = {}

	local function dispose()
		if not isDisposed then
			isDisposed = true
			local toDispose = children
			children = {}
			disposeChildren(toDispose)
		end
	end

	local function capture(child: dispose)
		if not isDisposed then
			table.insert(children, child)
		else
			error("attempted to track a child after the resource has been disposed")
		end
	end

	local function own<T...>(child: dispose, ...: T...): T...
		capture(child)
		return ...
	end

	local function handleError<T...>(ok: boolean, ...: T...): T...
		if not ok then
			dispose()
			error(select(1, ...) :: any)
		end
		return ...
	end

	return dispose, handleError(pcall(resourceFn, own))
end

return createResource


local types = require(script.types)
export type own = types.own
export type dispose = types.dispose
export type resource<A..., R...> = types.resource<A..., R...>

return {
	createResource = require(script.createResource),
	createCachedResource = require(script.createCachedResource),
}


export type own = <T...>(dispose, T...) -> T...

export type dispose = () -> ()

export type resource<A..., R...> = (A...) -> (dispose, R...)

return nil


local Root = script:FindFirstAncestor("SignalsExperimental")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local Signals = require(Packages.Signals)
local createEffect = Signals.createEffect

local SignalsScheduler = require(Packages.SignalsScheduler)
local batch = SignalsScheduler.batch

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

it("should crash when attempting to write to a read-only instance", function()
	local proxy = createProxy({
		foo = 123,
	})

	local store = proxy()

	expect(function()
		store.foo += 1
	end).toThrow()

	expect(function()
		store.baz = 1
	end).toThrow()
end)

it("should not re-execute if nothing is read", function()
	local count = 0

	local proxy = createProxy({
		foo = 1,
		bar = 100,
		baz = nil :: number?,
	})

	local dispose = createEffect(function(scope)
		count += 1
		proxy(scope)
	end)

	task.wait()
	expect(count).toEqual(1)

	proxy.foo += 5
	task.wait()
	expect(count).toEqual(1)

	proxy.bar += 5
	task.wait()
	expect(count).toEqual(1)

	proxy.foo += 5
	task.wait()
	expect(count).toEqual(1)

	proxy.baz = 5
	task.wait()
	expect(count).toEqual(1)

	proxy.baz = nil
	task.wait()
	expect(count).toEqual(1)

	dispose()
	proxy.bar += 5
	task.wait()
	expect(count).toEqual(1)
end)

it("should only notify as changed when accessed fields are changed", function()
	local proxy = createProxy({
		foo = 1,
		bar = 100,
	})

	local countFoo = 0

	local disposeFoo = createEffect(function(scope)
		countFoo += 1
		local _ = proxy(scope).foo
	end)

	task.wait()
	expect(countFoo).toEqual(1)
	expect(proxy().foo).toEqual(1)

	proxy.foo += 5
	task.wait()
	expect(countFoo).toEqual(2)
	expect(proxy().foo).toEqual(6)

	proxy.bar += 5
	task.wait()
	expect(countFoo).toEqual(2)
	expect(proxy().foo).toEqual(6)

	local countBar = 0
	local disposeBar = createEffect(function(scope)
		countBar += 1
		local _ = proxy(scope).bar
	end)

	task.wait()
	expect(countFoo).toEqual(2)
	expect(proxy().foo).toEqual(6)
	expect(countBar).toEqual(1)
	expect(proxy().bar).toEqual(105)

	proxy.bar += 5
	task.wait()
	expect(countFoo).toEqual(2)
	expect(proxy().foo).toEqual(6)
	expect(countBar).toEqual(2)
	expect(proxy().bar).toEqual(110)

	proxy.foo += 5
	task.wait()
	expect(countFoo).toEqual(3)
	expect(proxy().foo).toEqual(11)
	expect(countBar).toEqual(2)
	expect(proxy().bar).toEqual(110)

	proxy.foo += 5
	proxy.bar += 5
	task.wait()
	expect(countFoo).toEqual(4)
	expect(proxy().foo).toEqual(16)
	expect(countBar).toEqual(3)
	expect(proxy().bar).toEqual(115)

	disposeFoo()
	disposeBar()
end)

it("should support iterating over elements", function()
	local proxy = createProxy({
		foo = 1,
		bar = 100,
	} :: {
		foo: number,
		bar: number,
		baz: number?,
	})

	local count = 0
	local dispose = createEffect(function(scope)
		count += 1
		for key in proxy(scope) do
			local _ = key
		end
	end)

	task.wait()
	expect(count).toEqual(1)

	proxy.baz = 5
	task.wait()
	expect(count).toEqual(2)

	assert(proxy.baz, "non-nil")
	proxy.baz += 5
	task.wait()
	expect(count).toEqual(3)

	proxy.baz = nil
	task.wait()
	expect(count).toEqual(4)

	batch(function()
		proxy.foo += 5
		proxy.bar += 5
	end)
	task.wait()
	expect(count).toEqual(5)

	dispose()
end)

it("should support removing elements", function()
	local proxy = createProxy({} :: { [string]: number? })

	local count = 0
	local dispose = createEffect(function(scope)
		count += 1
		local _ = proxy(scope).foo
	end)

	task.wait()
	expect(count).toEqual(1)

	proxy.foo = nil
	task.wait()
	expect(count).toEqual(1)

	proxy.foo = 123
	task.wait()
	expect(count).toEqual(2)

	proxy.foo = 456
	task.wait()
	expect(count).toEqual(3)

	proxy.foo = nil
	task.wait()
	expect(count).toEqual(4)

	dispose()
end)

it("should tracking length", function()
	local proxy = createProxy({} :: { [number]: boolean?, foo: string? })

	local count = 0
	local dispose = createEffect(function(scope)
		count += 1
		local _ = #proxy(scope)
	end)

	task.wait()
	expect(count).toEqual(1)

	proxy.foo = "true"
	task.wait()
	expect(count).toEqual(1)

	proxy[1] = true
	task.wait()
	expect(count).toEqual(2)

	proxy[1] = false
	task.wait()
	expect(count).toEqual(2)

	proxy[2] = nil
	task.wait()
	expect(count).toEqual(2)

	proxy[2] = true
	task.wait()
	expect(count).toEqual(3)

	proxy[3] = true
	task.wait()
	expect(count).toEqual(4)

	proxy.foo = nil
	task.wait()
	expect(count).toEqual(4)

	proxy[2] = nil
	task.wait()
	expect(count).toEqual(5)

	proxy[1] = nil
	task.wait()
	expect(count).toEqual(6)

	proxy[3] = nil
	task.wait()
	expect(count).toEqual(7)

	dispose()
end)


local Root = script:FindFirstAncestor("SignalsExperimental")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local Signals = require(Packages.Signals)
local createSignal = Signals.createSignal

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

it("should allow tracking the last seen value", function()
	local getCurrent, setCurrent = createSignal(nil :: number?)

	local getLastSeen = createReducer(function(scope, previous: number?)
		return getCurrent(scope) or previous
	end, nil)

	expect(getLastSeen(false)).toEqual(nil)

	setCurrent(0)
	expect(getLastSeen(false)).toEqual(0)

	setCurrent(1)
	expect(getLastSeen(false)).toEqual(1)

	setCurrent(nil)
	expect(getLastSeen(false)).toEqual(1)

	setCurrent(2)
	expect(getLastSeen(false)).toEqual(2)

	setCurrent(nil)
	expect(getLastSeen(false)).toEqual(2)
end)


local Packages = script.Parent.Parent

local Signals = require(Packages.Signals)
local createSignal = Signals.createSignal
local createComputed = Signals.createComputed

local SignalsScheduler = require(Packages.SignalsScheduler)
local batch = SignalsScheduler.batch

type scope = Signals.scope
type getter<T> = Signals.getter<T>
type setter<T> = Signals.setter<T>

export type readonly<T> = T

export type proxy<T> = typeof(setmetatable({}, {} :: { __call: (unknown, scope?) -> readonly<T> })) & T

-- TODO: new solver
-- export type proxy<T> = setmetatable<T, { __call: (unknown, scope?) -> readonly<T> }>

local function createWrappedSignal<T>(initial: T): { getter: getter<T>, setter: setter<T> }
	local getter, setter = createSignal(function()
		return initial
	end)
	return { getter = getter, setter = setter }
end

local function createProxy<T>(initial: T & {}): proxy<T>
	local signals = {}

	for k, v in initial :: {} do
		signals[k] = createWrappedSignal(v)
	end

	local trackProxy, updateProxy = createSignal(nil, function()
		return false
	end)

	local getLength = createComputed(function(scope)
		trackProxy(scope)
		local length = 0
		for _, v in ipairs(signals :: {}) do
			if v.getter(false) ~= nil then
				length += 1
			end
		end
		return length
	end)

	local function createTrackedProxy(_, scope: scope | nil): T
		local function trackedLength()
			return getLength(scope)
		end

		local function nextSignal(state, index)
			while true do
				local nextIndex, nextValue = next(state, index)
				if nextIndex == nil then
					return nil
				end
				if nextValue ~= nil then
					local currentValue = nextValue.getter(scope)
					if currentValue ~= nil then
						return nextIndex, currentValue
					end
				end
				index = nextIndex
			end
		end

		local function trackedIterator()
			trackProxy(scope)
			return nextSignal, signals
		end

		local function trackedIndex(_, index): unknown
			local signal = signals[index]
			if signal == nil then
				if scope == nil then
					return nil
				end
				signal = createWrappedSignal(nil :: unknown)
				signals[index] = signal
			end
			return signal.getter(scope)
		end

		local function noNewIndex(_, index, value)
			error(`attempt to update read-only store at {index} with {value}`, 2)
		end

		local trackedMetatable = table.freeze({
			__len = trackedLength,
			__iter = trackedIterator,
			__index = trackedIndex,
			__newindex = noNewIndex,
			__metatable = false,
		})

		return (setmetatable({}, trackedMetatable) :: unknown) :: T
	end

	local function untrackedLength()
		return getLength(false)
	end

	local function nextSignal(state, index)
		while true do
			local nextIndex, nextValue = next(state, index)
			if nextIndex == nil then
				return nil
			end
			if nextValue ~= nil then
				local currentValue = nextValue.getter(false)
				if currentValue ~= nil then
					return nextIndex, currentValue
				end
			end
			index = nextIndex
		end
	end

	local function untrackedIterator()
		return nextSignal, signals
	end

	local function untrackedIndex(_, index)
		local signal = signals[index]
		if signal ~= nil then
			return signal.getter(false)
		end
		return nil
	end

	local function newIndex(_, index, value)
		local signal = signals[index]
		if signal == nil then
			if value ~= nil then
				signal = createWrappedSignal(value)
				signals[index] = signal
				updateProxy()
			end
		else
			local current = signal.getter(false)
			if current ~= value then
				batch(function()
					signal.setter(function()
						return value
					end)
					if current == nil or value == nil then
						updateProxy()
					end
				end)
			end
		end
	end

	local untrackedMetatable = table.freeze({
		__len = untrackedLength,
		__call = createTrackedProxy,
		__iter = untrackedIterator,
		__index = untrackedIndex,
		__newindex = newIndex,
		__metatable = false,
	})

	return (setmetatable({}, untrackedMetatable) :: unknown) :: proxy<T>
end

return createProxy


local Packages = script.Parent.Parent
local Signals = require(Packages.Signals)
local createComputed = Signals.createComputed

type getter<T> = Signals.getter<T>
type scope = Signals.scope

local function createReducer<T>(reducer: (scope, previous: T) -> T, initial: (() -> T) | T): getter<T>
	local isInitialized = false
	local state: T

	local function ensureInitialized()
		if not isInitialized then
			isInitialized = true
			state = if typeof(initial) == "function" then initial() else initial
		end
	end

	return createComputed(function(scope)
		ensureInitialized()
		state = reducer(scope, state)
		return state
	end)
end

return createReducer


local SignalsScheduler = require(script.Parent.SignalsScheduler)

local createProxy = require(script.createProxy)
export type proxy<T> = createProxy.proxy<T>

return {
	batch = SignalsScheduler.batch,
	createProxy = createProxy,
	createReducer = require(script.createReducer),
	onDisposed = require(script.onDisposed),
}


local Packages = script.Parent.Parent
local Signals = require(Packages.Signals)

type scope = Signals.scope
type dispose = Signals.dispose

local function onDisposed(scope: scope, dispose: dispose)
	local function source(_, delete: true?)
		if delete then
			dispose()
		end
		return 0
	end

	scope(source)
end

return onDisposed


local Signals = require(script.Parent.Parent.Signals)
local createExplicitSignal = Signals.createSignal
local createExplicitComputed = Signals.createComputed
local createExplicitEffect = Signals.createEffect

export type getter<T> = (false | nil) -> T
export type setter<T> = Signals.setter<T>
export type update<T> = Signals.update<T>
export type equals<T> = Signals.equals<T>
export type dispose = Signals.dispose

type scope = Signals.scope

local CurrentScope: scope? = nil

local function handleError(ok: boolean, ...)
	if not ok then
		local err = (...)
		error(err)
	end
end

local function handleResult<Ts...>(previousScope: scope?, ok: boolean, ...: Ts...): Ts...
	CurrentScope = previousScope
	handleError(ok, ...)
	return ...
end

local function createImplicitScopeFn<Ts...>(fn: () -> Ts...)
	local function wrappedFn(scope: scope): Ts...
		local previousScope: scope? = CurrentScope
		CurrentScope = scope
		return handleResult(previousScope, pcall(fn))
	end
	return wrappedFn
end

local function createImplicitGetter<T>(getter: Signals.getter<T>): getter<T>
	local function implicitGetter(untrack: false | nil): T
		return getter(if untrack == false then false else CurrentScope)
	end
	return implicitGetter
end

local function createSignal<T>(initial: (() -> T) | T, equals: equals<T>?): (getter<T>, setter<T>)
	local getter, setter = createExplicitSignal(initial, equals)
	local implicitGetter = createImplicitGetter(getter)
	return implicitGetter, setter
end

local function createComputed<T>(computed: () -> T, equals: equals<T>?): getter<T>
	local implicitComputed = createImplicitScopeFn(computed)
	local getter = createExplicitComputed(implicitComputed, equals)
	local implicitGetter = createImplicitGetter(getter)
	return implicitGetter
end

local function createEffect(effect: () -> ()): dispose
	local implicitEffect = createImplicitScopeFn(effect)
	return createExplicitEffect(implicitEffect)
end

return {
	createSignal = createSignal,
	createComputed = createComputed,
	createEffect = createEffect,
}


local Root = script:FindFirstAncestor("SignalsImplicitScope")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local SignalsImplicitScope = require(script.Parent.Parent.SignalsImplicitScope)
local createSignal = SignalsImplicitScope.createSignal
local createComputed = SignalsImplicitScope.createComputed
local createEffect = SignalsImplicitScope.createEffect

it("should track signals without explicit scope passing", function()
	local count = 0

	local getValue, setValue = createSignal(0)

	local dispose = createEffect(function()
		getValue()
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)

	setValue(1)
	task.wait()
	expect(count).toEqual(2)

	setValue(1)
	task.wait()
	expect(count).toEqual(2)

	setValue(2)
	task.wait()
	expect(count).toEqual(3)

	dispose()
	task.wait()

	setValue(3)
	task.wait()
	expect(count).toEqual(3)
end)

it("should track computeds without explicit scope passing", function()
	local count = 0

	local getValue, setValue = createSignal(0)

	local getDerived = createComputed(function()
		return getValue() + 1
	end)

	local dispose = createEffect(function()
		getDerived()
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)

	setValue(1)
	task.wait()
	expect(count).toEqual(2)

	setValue(1)
	task.wait()
	expect(count).toEqual(2)

	setValue(2)
	task.wait()
	expect(count).toEqual(3)

	dispose()
	task.wait()

	setValue(3)
	task.wait()
	expect(count).toEqual(3)
end)

it("should support untracked signals", function()
	local count = 0

	local getValue1, setValue1 = createSignal(0)
	local getValue2, setValue2 = createSignal(0)

	local dispose = createEffect(function()
		getValue1()
		getValue2(false)
		count += 1
	end)

	task.wait()
	expect(count).toEqual(1)

	setValue1(1)
	task.wait()
	expect(count).toEqual(2)

	setValue2(1)
	task.wait()
	expect(count).toEqual(2)

	setValue2(2)
	task.wait()
	expect(count).toEqual(2)

	setValue1(2)
	task.wait()
	expect(count).toEqual(3)

	dispose()
	task.wait()
end)


local SignalsImplicitScope = require(script.SignalsImplicitScope)

export type getter<T> = SignalsImplicitScope.getter<T>
export type setter<T> = SignalsImplicitScope.setter<T>
export type update<T> = SignalsImplicitScope.update<T>
export type equals<T> = SignalsImplicitScope.equals<T>
export type dispose = SignalsImplicitScope.dispose

return SignalsImplicitScope


local Root = script:FindFirstAncestor("SignalsReact")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it
local ReactRoblox = require(Packages.Dev.ReactRoblox)

local React = require(Packages.React)

local Signals = require(Packages.Signals)
local createSignal = Signals.createSignal
local createComputed = Signals.createComputed

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

it("should not re-render when the signal value changes", function()
	local renderCount = 0

	local getSignal, setSignal = createSignal("hello")

	local function mockComponent(props: { get: Signals.getter<string> })
		renderCount += 1
		local binding = useSignalBinding(props.get)
		return React.createElement("TextLabel", {
			Text = binding,
		})
	end

	local container = Instance.new("Folder")
	local root = ReactRoblox.createRoot(container)

	ReactRoblox.act(function()
		root:render(React.createElement(mockComponent, { get = getSignal }))
	end)

	local label = container:FindFirstChildWhichIsA("TextLabel")
	expect(label).never.toBeNil()
	assert(label, "should be non-nil")

	expect(renderCount).toEqual(2)
	expect(label.Text).toEqual("hello")

	ReactRoblox.act(function()
		setSignal("world")
		task.wait()
	end)

	expect(renderCount).toEqual(2)
	expect(label.Text).toEqual("world")

	ReactRoblox.act(function()
		setSignal("hi")
		task.wait()
	end)

	expect(renderCount).toEqual(2)
	expect(label.Text).toEqual("hi")

	local getDerived = createComputed(function(scope)
		return getSignal(scope)
	end)

	ReactRoblox.act(function()
		root:render(React.createElement(mockComponent, { get = getDerived }))
	end)

	expect(renderCount).toEqual(4)
	expect(label.Text).toEqual("hi")

	ReactRoblox.act(function()
		setSignal("bye")
		task.wait()
	end)

	expect(renderCount).toEqual(4)
	expect(label.Text).toEqual("bye")

	root:unmount()
	container:Destroy()
end)


local Root = script:FindFirstAncestor("SignalsReact")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it
local ReactRoblox = require(Packages.Dev.ReactRoblox)

local React = require(Packages.React)

local Signals = require(Packages.Signals)
local createSignal = Signals.createSignal
local createComputed = Signals.createComputed

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

local _, FFlagSignalsReactUseMutableSource = xpcall(function()
	return game:DefineFastFlag("SignalsReactUseMutableSource", false)
end, function()
	return true
end)

it("should return state synchronously", function()
	local rendered = {}

	local getSignal, setSignal = createSignal(1)

	local function mockComponent(props: { get: Signals.getter<number> })
		local value = useSignalState(props.get)
		table.insert(rendered, value)
	end

	local container = Instance.new("Folder")
	local root = ReactRoblox.createRoot(container)

	ReactRoblox.act(function()
		root:render(React.createElement(mockComponent, { get = getSignal }))
	end)

	expect(#rendered).toEqual(2)
	expect(rendered).toEqual({ 1, 1 })

	ReactRoblox.act(function()
		setSignal(2)
		task.wait()
	end)

	if FFlagSignalsReactUseMutableSource then
		expect(#rendered).toEqual(4)
		expect(rendered).toEqual({ 1, 1, 2, 2 })
	else
		expect(#rendered).toEqual(6)
		expect(rendered).toEqual({ 1, 1, 2, 2, 2, 2 })
	end

	ReactRoblox.act(function()
		setSignal(2)
		task.wait()
	end)

	if FFlagSignalsReactUseMutableSource then
		expect(#rendered).toEqual(4)
		expect(rendered).toEqual({ 1, 1, 2, 2 })
	else
		expect(#rendered).toEqual(6)
		expect(rendered).toEqual({ 1, 1, 2, 2, 2, 2 })
	end

	local getDerived = createComputed(function(scope)
		return getSignal(scope)
	end)

	ReactRoblox.act(function()
		root:render(React.createElement(mockComponent, { get = getDerived }))
	end)

	if FFlagSignalsReactUseMutableSource then
		expect(#rendered).toEqual(6)
		expect(rendered).toEqual({ 1, 1, 2, 2, 2, 2 })
	else
		expect(#rendered).toEqual(8)
		expect(rendered).toEqual({ 1, 1, 2, 2, 2, 2, 2, 2 })
	end

	ReactRoblox.act(function()
		setSignal(3)
		task.wait()
	end)

	if FFlagSignalsReactUseMutableSource then
		expect(#rendered).toEqual(8)
		expect(rendered).toEqual({ 1, 1, 2, 2, 2, 2, 3, 3 })
	else
		expect(#rendered).toEqual(12)
		expect(rendered).toEqual({ 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3 })
	end

	root:unmount()
	container:Destroy()
end)


return {
	useSignalBinding = require(script.useSignalBinding),
	useSignalState = require(script.useSignalState),
}


local Packages = script.Parent.Parent
local React = require(Packages.React)
type Binding<T> = React.Binding<T>

local Signals = require(Packages.Signals)
local createEffect = Signals.createEffect
type getter<T> = Signals.getter<T>

local function useSignalBinding<T>(getter: getter<T>): Binding<T>
	local binding, setBinding = React.useBinding(getter(false))

	React.useLayoutEffect(function()
		setBinding(getter(false))
		return createEffect(function(scope)
			setBinding(getter(scope))
		end)
	end, { getter })

	return binding
end

return useSignalBinding


local Packages = script.Parent.Parent
local React = require(Packages.React)

local Signals = require(Packages.Signals)
local createEffect = Signals.createEffect
type getter<T> = Signals.getter<T>

local _, FFlagSignalsReactUseMutableSource = xpcall(function()
	return game:DefineFastFlag("SignalsReactUseMutableSource", false)
end, function()
	return true
end)

local function getSignalVersion(getter: getter<unknown>): number
	local function dummyObserver(): ()
		return getter -- here to ensure this function object is unique
	end

	-- these are duplicated from the core implementation
	type observer = () -> ()
	type source = (observer?, true?) -> number

	local capturedSource: source

	local function scope(source: source): observer
		capturedSource = source
		return dummyObserver
	end

	getter(scope)

	capturedSource(dummyObserver, true)

	return capturedSource()
end

local function getSignalSnapshot<T>(getter: getter<T>): T
	return getter(false)
end

local function subscribeToSignal<T>(getter: getter<T>, callback: (T) -> ())
	return createEffect(function(scope)
		callback(getter(scope))
	end)
end

local function useSignalState<T>(getter: getter<T>): T
	local signalSource = React.useMemo(function()
		return React.createMutableSource(getter, getSignalVersion)
	end, { getter })

	return React.useMutableSource(signalSource, getSignalSnapshot, subscribeToSignal)
end

local function useSignalState_old<T>(getter: getter<T>): T
	local value, setValue = React.useState(function()
		return getter(false)
	end)

	React.useLayoutEffect(function()
		setValue(getter(false))
		return createEffect(function(scope)
			setValue(getter(scope))
		end)
	end, { getter })

	return value
end

return if FFlagSignalsReactUseMutableSource then useSignalState else useSignalState_old :: never


local Root = script:FindFirstAncestor("SignalsRoblox")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local Signals = require(Packages.Signals)
local createSignal = Signals.createSignal

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

local function waitFrames(n: number)
	for _ = 1, n do
		task.wait()
	end
end

it("should support calling dispose from within the effect directly and only", function()
	local counter = 0

	local getValue, setValue = createSignal(1)

	createDetachedEffect(function(scope, dispose)
		if getValue(scope) >= 5 then
			dispose()
		else
			counter += 1
		end
	end)

	waitFrames(10)
	expect(counter).toEqual(1)
	waitFrames(10)
	expect(counter).toEqual(1)

	setValue(2)
	waitFrames(10)
	expect(counter).toEqual(2)

	setValue(2)
	waitFrames(10)
	expect(counter).toEqual(2)

	setValue(3)
	waitFrames(10)
	expect(counter).toEqual(3)

	setValue(4)
	waitFrames(10)
	expect(counter).toEqual(4)

	setValue(5) -- disposed here!
	waitFrames(10)
	expect(counter).toEqual(4)

	setValue(1)
	waitFrames(10)
	expect(counter).toEqual(4)
end)


local Root = script:FindFirstAncestor("SignalsRoblox")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local Signals = require(Packages.Signals)

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

it("should create a signal from an RBXScriptSignal", function()
	local bindableEvent = Instance.new("BindableEvent")
	local getter: Signals.getter<any>, connection: RBXScriptConnection = createRBXEventSignal(bindableEvent.Event, 0)

	expect(getter).toBeDefined()
	expect(connection).toBeDefined()

	bindableEvent:Fire(1)
	expect(getter(false)).toEqual(1)

	connection:Disconnect()
end)

it("should create a signal from an RBXScriptSignal with an initial value", function()
	local bindableEvent = Instance.new("BindableEvent")
	local getter: Signals.getter<any>, connection: RBXScriptConnection = createRBXEventSignal(bindableEvent.Event, 1)

	expect(getter).toBeDefined()
	expect(connection).toBeDefined()

	expect(getter(false)).toEqual(1)
	bindableEvent:Fire(2)
	expect(getter(false)).toEqual(2)

	connection:Disconnect()
end)

it("should create a signal from an RBXScriptSignal with a filter", function()
	local bindableEvent = Instance.new("BindableEvent")
	local getter: Signals.getter<any>, connection: RBXScriptConnection = createRBXEventSignal(
		bindableEvent.Event,
		0,
		function(value: number)
			return value + 2
		end
	)

	expect(getter).toBeDefined()
	expect(connection).toBeDefined()

	bindableEvent:Fire(1)
	expect(getter(false)).toEqual(3)

	connection:Disconnect()
end)


local Root = script.Parent
local Packages = Root.Parent
local Signals = require(Packages.Signals)
local createEffect = Signals.createEffect

type scope = Signals.scope
type dispose = Signals.dispose

local function createDetachedEffect(effect: (scope, dispose) -> ())
	local isDisposed = false
	local event: BindableEvent? = nil

	local function disposeEffect()
		if not isDisposed then
			isDisposed = true
			if event ~= nil then
				event:Fire()
			end
		end
	end

	local dispose = createEffect(function(scope)
		effect(scope, disposeEffect)
	end)

	if isDisposed then
		dispose()
	else
		local bindable = Instance.new("BindableEvent")
		event = bindable
		bindable.Event:Once(function()
			bindable:Destroy()
			event = nil
			dispose()
		end)
	end
end

return createDetachedEffect


local Root = script.Parent
local Packages = Root.Parent
local Signals = require(Packages.Signals)
local createSignal = Signals.createSignal

type getter<T> = Signals.getter<T>
type scope = Signals.scope
type dispose = Signals.dispose

local function defaultFilter(...: any): any
	return select(1, ...)
end

local function createRBXEventSignal<T>(
	event: RBXScriptSignal,
	initialValue: (() -> T) | T,
	filter: ((...any) -> T)?
): (getter<T>, RBXScriptConnection)
	local getter, setter = createSignal(initialValue)

	local filterEvent: (...any) -> T = if filter ~= nil then filter else defaultFilter

	local connection = event:Connect(function(...: any)
		setter(filterEvent(...))
	end)

	return getter, connection
end

return createRBXEventSignal


return {
	createRBXEventSignal = require(script.createRBXEventSignal),
	createDetachedEffect = require(script.createDetachedEffect),
}


export type work = () -> ()

local isContinuing = false

local continuations: { work } = {}

local function batch(fn: work)
	if not isContinuing then
		isContinuing = true
		fn()
		for _, work in continuations do
			work()
		end
		table.clear(continuations)
		isContinuing = false
	else
		fn()
	end
end

local function flush()
	batch(function() end)
end

local function schedule(work: work)
	table.insert(continuations, work)
end

return {
	batch = batch,
	flush = flush,
	schedule = schedule,
}


local Root = script:FindFirstAncestor("SignalsScheduler")
local Packages = Root.Parent

local JestGlobals = require(Packages.Dev.JestGlobals)
local expect = JestGlobals.expect
local it = JestGlobals.it

local SignalsScheduler = require(script.Parent.Parent.SignalsScheduler)
local batch = SignalsScheduler.batch
local flush = SignalsScheduler.flush
local schedule = SignalsScheduler.schedule

it("should schedule work without running until flushed", function()
	local counters = {
		foo = 0,
		bar = 0,
	}

	local function foo()
		counters.foo += 1
	end

	local function bar()
		counters.bar += 1
	end

	schedule(foo)
	schedule(bar)

	task.wait()

	expect(counters).toEqual({
		foo = 0,
		bar = 0,
	})

	flush()

	expect(counters).toEqual({
		foo = 1,
		bar = 1,
	})

	schedule(foo)
	schedule(foo)
	schedule(foo)
	schedule(bar)

	expect(counters).toEqual({
		foo = 1,
		bar = 1,
	})

	flush()

	expect(counters).toEqual({
		foo = 4,
		bar = 2,
	})
end)

it("should support incremental scheduling", function()
	local counters = {
		foo = 0,
		bar = 0,
		baz = 0,
	}

	local function foo()
		counters.foo += 1
	end

	local function bar()
		schedule(foo)
		counters.bar += 1
	end

	local function baz()
		schedule(bar)
		counters.baz += 1
	end

	schedule(baz)

	task.wait()

	expect(counters).toEqual({
		foo = 0,
		bar = 0,
		baz = 0,
	})

	flush()

	expect(counters).toEqual({
		foo = 1,
		bar = 1,
		baz = 1,
	})
end)

it("should support batching scheduled work", function()
	local counters = {
		foo = 0,
		bar = 0,
	}

	local function foo()
		counters.foo += 1
	end

	local function bar()
		counters.bar += 1
	end

	batch(function()
		schedule(foo)
		schedule(bar)
	end)

	task.wait()

	expect(counters).toEqual({
		foo = 1,
		bar = 1,
	})

	flush()

	expect(counters).toEqual({
		foo = 1,
		bar = 1,
	})
end)


local SignalsScheduler = require(script.SignalsScheduler)

export type work = SignalsScheduler.work

return SignalsScheduler


local Packages = script.Parent.Parent
local SignalsScheduler = require(Packages.SignalsScheduler)
local flush = SignalsScheduler.flush
local schedule = SignalsScheduler.schedule

local callUserSpace = require(script.Parent.callUserSpace)

export type getter<T> = (scope | false | nil) -> T
export type setter<T> = (update<T>) -> ()
export type update<T> = ((previous: T) -> T) | T
export type equals<T> = (current: T, incoming: T) -> boolean
export type dispose = () -> ()

-- The "scope" function is used by a source (signals and computeds) to register itself with
-- an observer (computeds and effects) when the source is read
export type scope = (source) -> observer

-- The "source" function is used by an observer to:
-- 1. Ask the source to update, then return its latest version
--		() -> number
-- 2. Rettach an observer to the source
--		(observer) -> ()
-- 3. Remove an observer from the source
--		(observer, true) -> ()
type source = (observer?, true?) -> number

-- The "observer" function is used by a source to notify the observer that one of its sources may be stale
type observer = () -> ()

type set<T> = { [T]: true? }
local WeakSetMetatable = table.freeze({ __mode = "k" })
local function createWeakSet<T>(set: set<T>)
	return (setmetatable(set, WeakSetMetatable) :: unknown) :: set<T>
end

local function defaultEquals<T>(current: T, incoming: T)
	return current == incoming
end

local function handleError(ok: boolean, ...)
	if not ok then
		local err = (...)
		error(err)
	end
end

local function handleScopeValidation<Ts...>(kill: () -> (), ok: boolean, ...: Ts...): Ts...
	kill()
	handleError(ok, ...)
	return ...
end

local function callUserSpaceWithScopeValidation<Ts...>(fn: (scope) -> Ts..., scope: scope): Ts...
	local isAlive = true

	local function wrappedScope(source: source)
		if not isAlive then
			error("attempted to use scope beyond scope's lifetime")
		end
		return scope(source)
	end

	local function kill()
		isAlive = false
	end

	return handleScopeValidation(kill, pcall(callUserSpace, fn, wrappedScope))
end

local validationEnabled = _G.__SIGNALS_VALIDATION_ENABLED__ or _G.__DEV__
local callUserSpaceWithScope = if validationEnabled then callUserSpaceWithScopeValidation else callUserSpace :: never

local function createSignal<T>(initial: (() -> T) | T, equals: equals<T>?): (getter<T>, setter<T>)
	local isInitialized = false
	local version = 0

	local value: T
	local observers: set<observer>

	local isEqual: equals<T> = if equals ~= nil then equals else defaultEquals

	local function ensureInitialized()
		if not isInitialized then
			isInitialized = true
			value = if typeof(initial) == "function" then callUserSpace(initial) else initial
			version = os.clock()
			observers = createWeakSet({})
		end
	end

	local function source(childObserver: observer?, delete: true?)
		if childObserver ~= nil then
			if delete then
				observers[childObserver] = nil
			else
				observers[childObserver] = true
			end
			return 0
		else
			return version
		end
	end

	local function connectToScope(requestor: scope | false | nil)
		if requestor then
			local childObserver = requestor(source)
			observers[childObserver] = true
		end
	end

	local function notifyObservers()
		for childObserver in observers do
			childObserver()
		end
		table.clear(observers)
	end

	local function getter(requestor: scope | false | nil): T
		ensureInitialized()
		connectToScope(requestor)
		return value
	end

	local function setter(update: update<T>)
		ensureInitialized()
		local newValue = if typeof(update) == "function" then callUserSpace(update, value) else update
		if not callUserSpace(isEqual, value, newValue) then
			value = newValue
			version = os.clock()
			notifyObservers()
			flush()
		end
	end

	return getter, setter
end

local function createComputed<T>(computed: (scope) -> T, equals: equals<T>?): getter<T>
	local isInitialized = false
	local isStale = false
	local cachedVersion = 0
	local absoluteVersion = 0

	local value: T
	local sources: set<source>
	local observers: set<observer>

	local isEqual: equals<T> = if equals ~= nil then equals else defaultEquals

	local function notifyObservers()
		for childObserver in observers do
			childObserver()
		end
		table.clear(observers)
	end

	local function observer()
		if not isStale then
			isStale = true
			notifyObservers()
		end
	end

	local function scope(parentSource: source)
		sources[parentSource] = true
		return observer
	end

	local function ensureInitialized()
		if not isInitialized then
			isInitialized = true
			observers = createWeakSet({})
			sources = {}
			value = callUserSpaceWithScope(computed, scope)
			absoluteVersion = os.clock()
			cachedVersion = absoluteVersion
		end
	end

	local function disconnectSources()
		for parentSource in sources do
			parentSource(observer, true)
		end
		table.clear(sources)
	end

	local function flushNotifications()
		if isStale then
			isStale = false
			for parentSource in sources do
				local newVersion = parentSource()
				if newVersion > absoluteVersion then
					disconnectSources()
					local newValue = callUserSpaceWithScope(computed, scope)
					absoluteVersion = os.clock()
					if not callUserSpace(isEqual, value, newValue) then
						value = newValue
						cachedVersion = absoluteVersion
					end
					return
				end
			end
			for parentSource in sources do
				parentSource(observer)
			end
		end
	end

	local function source(childObserver: observer?, delete: true?)
		if childObserver ~= nil then
			if delete then
				observers[childObserver] = nil
			else
				observers[childObserver] = true
			end
			return 0
		else
			flushNotifications()
			return cachedVersion
		end
	end

	local function connectToScope(requestor: scope | false | nil)
		if requestor then
			local childObserver = requestor(source)
			observers[childObserver] = true
		end
	end

	local function getter(requestor: scope | false | nil): T
		ensureInitialized()
		flushNotifications()
		connectToScope(requestor)
		return value
	end

	return getter
end

local function createEffect(effect: (scope) -> ()): dispose
	local isScheduled = false
	local isDisposed = false
	local version = 0

	local sources: set<source> = {}

	local observer: observer

	local function disconnectSources()
		for source in sources do
			source(observer, true)
		end
		table.clear(sources)
	end

	local function dispose()
		isDisposed = true
		disconnectSources()
	end

	local function scope(parentSource: source)
		sources[parentSource] = true
		return observer
	end

	local function processNotification()
		if not isDisposed then
			isScheduled = false
			for parentSource in sources do
				local newVersion = parentSource()
				if newVersion > version then
					disconnectSources()
					callUserSpaceWithScope(effect, scope)
					version = os.clock()
					return
				end
			end
			for parentSource in sources do
				parentSource(observer)
			end
		end
	end

	function observer()
		if not isDisposed then
			if not isScheduled then
				isScheduled = true
				schedule(processNotification)
			end
		end
	end

	callUserSpaceWithScope(effect, scope)
	version = os.clock()

	return dispose
end

return {
	createSignal = createSignal,
	createComputed = createComputed,
	createEffect = createEffect,
}