Compare commits

...

48 commits

Author SHA1 Message Date
EternityDev
0b1304f4c5 v1.0.14 2024-12-02 14:37:46 +07:00
EternityDev
d8526c7e25 pre v1.0.14 2024-12-01 21:10:56 +07:00
EternityDev
d065bc2e50 oops 2024-11-30 23:55:07 +07:00
EternityDev
925df47d4c improvements 2024-11-30 23:49:37 +07:00
EternityDev
f3b674b377 rewrite buffer instance type serialization for fix issues 2024-11-23 19:51:14 +07:00
EternityDev
677d3fa675 rewrite buffer instance type serialization for fix issues 2024-11-23 19:50:06 +07:00
EternityDev
91862a65fe add more type support to buffer 2024-11-21 13:57:53 +07:00
EternityDev
8ba9540550 update .rbxm 2024-11-21 07:11:47 +07:00
EternityDev
1d67954ef9 fix & improved array type 2024-11-21 06:59:55 +07:00
EternityDev
43c4a1594f fixes to v1.0.13 2024-11-20 10:25:00 +07:00
EternityDev
22996c9357 v1.0.13 2024-11-19 18:24:46 +07:00
EternityDev
a377788f22 patch oudated deploy.yml 2024-10-05 18:41:32 +07:00
EternityDev
3354324c5b patch outdated deploy.yml 2024-10-05 18:40:12 +07:00
EternityDev
20b97eeb54 v1.0.13 .rbxm 2024-09-27 11:14:33 +07:00
EternityDev
dbed984eea v1.0.13 2024-09-27 11:10:12 +07:00
EternityDev
064075fbd9 v1.0.12 2024-05-29 16:58:20 +07:00
EternityDev
77de85b6b8 update: .rbxm file 2024-05-25 22:39:07 +07:00
EternityDev
10de54608a Fix: server invoke 2024-05-25 22:33:58 +07:00
EternityDev
5b2e36b7bb
Merge pull request #23 from lhkzh/patch-2
Fix bug: unpack server invoke params error
2024-05-25 22:31:14 +07:00
EternityDev
eba9f79655 Fix: Destroying a Signal 2024-05-24 20:52:00 +07:00
lhkzh
7309840005
Update init.luau
fix bug: unpack server invoke params error
2024-05-24 17:43:22 +08:00
EternityDev
aa693aee4f v1.0.11 2024-05-19 13:17:07 +07:00
EternityDev
44fa07df85 update .rbxm file 2024-05-11 09:31:23 +07:00
EternityDev
839a7af667 v1.0.10 2024-05-11 09:29:53 +07:00
EternityDev
acb08a385c improved ClientProcess 2024-05-10 16:35:09 +07:00
EternityDev
6acf92d913 improved ServerProcess 2024-05-10 16:15:39 +07:00
EternityDev
b4ee5dc1e3 fix serdes 2024-05-10 13:07:10 +07:00
EternityDev
598c30c147 v1.0.10-test 2024-05-04 12:49:50 +07:00
EternityDev
0fb349fe0f minor improvement 2024-04-13 09:00:52 +07:00
EternityDev
4cd0f3f2cf set memory 2024-04-12 19:15:22 +07:00
EternityDev
8fd9573b8a fixes 2024-04-09 15:49:06 +07:00
EternityDev
739e13537d minor improvement 2024-04-08 12:18:13 +07:00
EternityDev
46bbe5feb0 Merge branch 'master' of https://github.com/imezx/Warp 2024-04-06 11:22:44 +07:00
EternityDev
0bc8f56e11 change 2024-04-06 11:21:35 +07:00
EternityDev
69fc64d134
Merge pull request #10 from SpiralAPI/master
fixed rojo sync errors as well as safety checks
2024-04-04 20:56:17 +07:00
SpiralAPI
c8199d5744 fixed rojo sync errors as well as safety checks
for wally.toml
2024-04-04 09:49:14 -04:00
EternityDev
ef4b741f85
Merge pull request #8 from xArshy/v1.0.9
Removed ClientProcess.luau
2024-04-02 20:07:35 +07:00
xArshy
e7b4ae2f61 Redudant ClientProcess.luau, removes conflict 2024-04-02 17:06:18 +04:00
EternityDev
9aaa382278 v1.0.9 2024-04-02 13:10:21 +07:00
EternityDev
5cf44bd21f patch 2024-03-24 16:36:31 +07:00
EternityDev
5b0d4ee690 1.0.8 2024-03-18 12:50:04 +07:00
EternityDev
7519839dd1 fix 2024-03-17 16:04:48 +07:00
EternityDev
01c5533394 expanding buffer 2024-03-16 23:49:23 +07:00
EternityDev
4b568356c8 v1.0.8 2024-03-14 11:58:08 +07:00
EternityDev
ae1754d616 reupdate v1.0.7 2024-03-13 08:13:01 +07:00
EternityDev
563af05c62 v1.0.7 2024-03-13 08:01:27 +07:00
EternityDev
f5bb1df4b0 1.0.6 2024-03-02 23:43:36 +07:00
EternityDev
8cad93f2f5 remove a duplicate of signal 2024-02-06 07:46:50 +07:00
50 changed files with 3436 additions and 666 deletions

View file

@ -18,16 +18,16 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: npm cache: npm
- name: Setup Pages - name: Setup Pages
uses: actions/configure-pages@v3 uses: actions/configure-pages@v4
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build - name: Build
@ -35,9 +35,9 @@ jobs:
npm run docs:build npm run docs:build
touch docs/.vitepress/dist touch docs/.vitepress/dist
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@v2 uses: actions/upload-pages-artifact@v3
with: with:
path: docs/.vitepress/dist path: docs/.vitepress/dist
- name: Deploy - name: Deploy
id: deployment id: deployment
uses: actions/deploy-pages@v2 uses: actions/deploy-pages@v4

2
.gitignore vendored
View file

@ -2,3 +2,5 @@ node_modules
docs/.vitepress/dist docs/.vitepress/dist
docs/.vitepress/cache docs/.vitepress/cache
wally.lock wally.lock
TestEZ
test.project.json

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"stylua.targetReleaseVersion": "latest"
}

26
TestEZ/Context.lua Normal file
View file

@ -0,0 +1,26 @@
--[[
The Context object implements a write-once key-value store. It also allows
for a new Context object to inherit the entries from an existing one.
]]
local Context = {}
function Context.new(parent)
local meta = {}
local index = {}
meta.__index = index
if parent then
for key, value in pairs(getmetatable(parent).__index) do
index[key] = value
end
end
function meta.__newindex(_obj, key, value)
assert(index[key] == nil, string.format("Cannot reassign %s in context", tostring(key)))
index[key] = value
end
return setmetatable({}, meta)
end
return Context

311
TestEZ/Expectation.lua Normal file
View file

@ -0,0 +1,311 @@
--[[
Allows creation of expectation statements designed for behavior-driven
testing (BDD). See Chai (JS) or RSpec (Ruby) for examples of other BDD
frameworks.
The Expectation class is exposed to tests as a function called `expect`:
expect(5).to.equal(5)
expect(foo()).to.be.ok()
Expectations can be negated using .never:
expect(true).never.to.equal(false)
Expectations throw errors when their conditions are not met.
]]
local Expectation = {}
--[[
These keys don't do anything except make expectations read more cleanly
]]
local SELF_KEYS = {
to = true,
be = true,
been = true,
have = true,
was = true,
at = true,
}
--[[
These keys invert the condition expressed by the Expectation.
]]
local NEGATION_KEYS = {
never = true,
}
--[[
Extension of Lua's 'assert' that lets you specify an error level.
]]
local function assertLevel(condition, message, level)
message = message or "Assertion failed!"
level = level or 1
if not condition then
error(message, level + 1)
end
end
--[[
Returns a version of the given method that can be called with either . or :
]]
local function bindSelf(self, method)
return function(firstArg, ...)
if firstArg == self then
return method(self, ...)
else
return method(self, firstArg, ...)
end
end
end
local function formatMessage(result, trueMessage, falseMessage)
if result then
return trueMessage
else
return falseMessage
end
end
--[[
Create a new expectation
]]
function Expectation.new(value)
local self = {
value = value,
successCondition = true,
condition = false,
matchers = {},
_boundMatchers = {},
}
setmetatable(self, Expectation)
self.a = bindSelf(self, self.a)
self.an = self.a
self.ok = bindSelf(self, self.ok)
self.equal = bindSelf(self, self.equal)
self.throw = bindSelf(self, self.throw)
self.near = bindSelf(self, self.near)
return self
end
function Expectation.checkMatcherNameCollisions(name)
if SELF_KEYS[name] or NEGATION_KEYS[name] or Expectation[name] then
return false
end
return true
end
function Expectation:extend(matchers)
self.matchers = matchers or {}
for name, implementation in pairs(self.matchers) do
self._boundMatchers[name] = bindSelf(self, function(_self, ...)
local result = implementation(self.value, ...)
local pass = result.pass == self.successCondition
assertLevel(pass, result.message, 3)
self:_resetModifiers()
return self
end)
end
return self
end
function Expectation.__index(self, key)
-- Keys that don't do anything except improve readability
if SELF_KEYS[key] then
return self
end
-- Invert your assertion
if NEGATION_KEYS[key] then
local newExpectation = Expectation.new(self.value):extend(self.matchers)
newExpectation.successCondition = not self.successCondition
return newExpectation
end
if self._boundMatchers[key] then
return self._boundMatchers[key]
end
-- Fall back to methods provided by Expectation
return Expectation[key]
end
--[[
Called by expectation terminators to reset modifiers in a statement.
This makes chains like:
expect(5)
.never.to.equal(6)
.to.equal(5)
Work as expected.
]]
function Expectation:_resetModifiers()
self.successCondition = true
end
--[[
Assert that the expectation value is the given type.
expect(5).to.be.a("number")
]]
function Expectation:a(typeName)
local result = (type(self.value) == typeName) == self.successCondition
local message = formatMessage(self.successCondition,
("Expected value of type %q, got value %q of type %s"):format(
typeName,
tostring(self.value),
type(self.value)
),
("Expected value not of type %q, got value %q of type %s"):format(
typeName,
tostring(self.value),
type(self.value)
)
)
assertLevel(result, message, 3)
self:_resetModifiers()
return self
end
-- Make alias public on class
Expectation.an = Expectation.a
--[[
Assert that our expectation value is truthy
]]
function Expectation:ok()
local result = (self.value ~= nil) == self.successCondition
local message = formatMessage(self.successCondition,
("Expected value %q to be non-nil"):format(
tostring(self.value)
),
("Expected value %q to be nil"):format(
tostring(self.value)
)
)
assertLevel(result, message, 3)
self:_resetModifiers()
return self
end
--[[
Assert that our expectation value is equal to another value
]]
function Expectation:equal(otherValue)
local result = (self.value == otherValue) == self.successCondition
local message = formatMessage(self.successCondition,
("Expected value %q (%s), got %q (%s) instead"):format(
tostring(otherValue),
type(otherValue),
tostring(self.value),
type(self.value)
),
("Expected anything but value %q (%s)"):format(
tostring(otherValue),
type(otherValue)
)
)
assertLevel(result, message, 3)
self:_resetModifiers()
return self
end
--[[
Assert that our expectation value is equal to another value within some
inclusive limit.
]]
function Expectation:near(otherValue, limit)
assert(type(self.value) == "number", "Expectation value must be a number to use 'near'")
assert(type(otherValue) == "number", "otherValue must be a number")
assert(type(limit) == "number" or limit == nil, "limit must be a number or nil")
limit = limit or 1e-7
local result = (math.abs(self.value - otherValue) <= limit) == self.successCondition
local message = formatMessage(self.successCondition,
("Expected value to be near %f (within %f) but got %f instead"):format(
otherValue,
limit,
self.value
),
("Expected value to not be near %f (within %f) but got %f instead"):format(
otherValue,
limit,
self.value
)
)
assertLevel(result, message, 3)
self:_resetModifiers()
return self
end
--[[
Assert that our functoid expectation value throws an error when called.
An optional error message can be passed to assert that the error message
contains the given value.
]]
function Expectation:throw(messageSubstring)
local ok, err = pcall(self.value)
local result = ok ~= self.successCondition
if messageSubstring and not ok then
if self.successCondition then
result = err:find(messageSubstring, 1, true) ~= nil
else
result = err:find(messageSubstring, 1, true) == nil
end
end
local message
if messageSubstring then
message = formatMessage(self.successCondition,
("Expected function to throw an error containing %q, but it %s"):format(
messageSubstring,
err and ("threw: %s"):format(err) or "did not throw."
),
("Expected function to never throw an error containing %q, but it threw: %s"):format(
messageSubstring,
tostring(err)
)
)
else
message = formatMessage(self.successCondition,
"Expected function to throw an error, but it did not throw.",
("Expected function to succeed, but it threw an error: %s"):format(
tostring(err)
)
)
end
assertLevel(result, message, 3)
self:_resetModifiers()
return self
end
return Expectation

View file

@ -0,0 +1,38 @@
local Expectation = require(script.Parent.Expectation)
local checkMatcherNameCollisions = Expectation.checkMatcherNameCollisions
local function copy(t)
local result = {}
for key, value in pairs(t) do
result[key] = value
end
return result
end
local ExpectationContext = {}
ExpectationContext.__index = ExpectationContext
function ExpectationContext.new(parent)
local self = {
_extensions = parent and copy(parent._extensions) or {},
}
return setmetatable(self, ExpectationContext)
end
function ExpectationContext:startExpectationChain(...)
return Expectation.new(...):extend(self._extensions)
end
function ExpectationContext:extend(config)
for key, value in pairs(config) do
assert(self._extensions[key] == nil, string.format("Cannot reassign %q in expect.extend", key))
assert(checkMatcherNameCollisions(key), string.format("Cannot overwrite matcher %q; it already exists", key))
self._extensions[key] = value
end
end
return ExpectationContext

89
TestEZ/LifecycleHooks.lua Normal file
View file

@ -0,0 +1,89 @@
local TestEnum = require(script.Parent.TestEnum)
local LifecycleHooks = {}
LifecycleHooks.__index = LifecycleHooks
function LifecycleHooks.new()
local self = {
_stack = {},
}
return setmetatable(self, LifecycleHooks)
end
--[[
Returns an array of `beforeEach` hooks in FIFO order
]]
function LifecycleHooks:getBeforeEachHooks()
local key = TestEnum.NodeType.BeforeEach
local hooks = {}
for _, level in ipairs(self._stack) do
for _, hook in ipairs(level[key]) do
table.insert(hooks, hook)
end
end
return hooks
end
--[[
Returns an array of `afterEach` hooks in FILO order
]]
function LifecycleHooks:getAfterEachHooks()
local key = TestEnum.NodeType.AfterEach
local hooks = {}
for _, level in ipairs(self._stack) do
for _, hook in ipairs(level[key]) do
table.insert(hooks, 1, hook)
end
end
return hooks
end
--[[
Pushes uncalled beforeAll and afterAll hooks back up the stack
]]
function LifecycleHooks:popHooks()
table.remove(self._stack, #self._stack)
end
function LifecycleHooks:pushHooksFrom(planNode)
assert(planNode ~= nil)
table.insert(self._stack, {
[TestEnum.NodeType.BeforeAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeAll),
[TestEnum.NodeType.AfterAll] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterAll),
[TestEnum.NodeType.BeforeEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.BeforeEach),
[TestEnum.NodeType.AfterEach] = self:_getHooksOfType(planNode.children, TestEnum.NodeType.AfterEach),
})
end
--[[
Get the beforeAll hooks from the current level.
]]
function LifecycleHooks:getBeforeAllHooks()
return self._stack[#self._stack][TestEnum.NodeType.BeforeAll]
end
--[[
Get the afterAll hooks from the current level.
]]
function LifecycleHooks:getAfterAllHooks()
return self._stack[#self._stack][TestEnum.NodeType.AfterAll]
end
function LifecycleHooks:_getHooksOfType(nodes, key)
local hooks = {}
for _, node in ipairs(nodes) do
if node.type == key then
table.insert(hooks, node.callback)
end
end
return hooks
end
return LifecycleHooks

View file

@ -0,0 +1,102 @@
local TestService = game:GetService("TestService")
local TestEnum = require(script.Parent.Parent.TestEnum)
local TeamCityReporter = {}
local function teamCityEscape(str)
str = string.gsub(str, "([]|'[])","|%1")
str = string.gsub(str, "\r", "|r")
str = string.gsub(str, "\n", "|n")
return str
end
local function teamCityEnterSuite(suiteName)
return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName))
end
local function teamCityLeaveSuite(suiteName)
return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName))
end
local function teamCityEnterCase(caseName)
return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName))
end
local function teamCityLeaveCase(caseName)
return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName))
end
local function teamCityFailCase(caseName, errorMessage)
return string.format("##teamcity[testFailed name='%s' message='%s']",
teamCityEscape(caseName), teamCityEscape(errorMessage))
end
local function reportNode(node, buffer, level)
buffer = buffer or {}
level = level or 0
if node.status == TestEnum.TestStatus.Skipped then
return buffer
end
if node.planNode.type == TestEnum.NodeType.Describe then
table.insert(buffer, teamCityEnterSuite(node.planNode.phrase))
for _, child in ipairs(node.children) do
reportNode(child, buffer, level + 1)
end
table.insert(buffer, teamCityLeaveSuite(node.planNode.phrase))
else
table.insert(buffer, teamCityEnterCase(node.planNode.phrase))
if node.status == TestEnum.TestStatus.Failure then
table.insert(buffer, teamCityFailCase(node.planNode.phrase, table.concat(node.errors,"\n")))
end
table.insert(buffer, teamCityLeaveCase(node.planNode.phrase))
end
end
local function reportRoot(node)
local buffer = {}
for _, child in ipairs(node.children) do
reportNode(child, buffer, 0)
end
return buffer
end
local function report(root)
local buffer = reportRoot(root)
return table.concat(buffer, "\n")
end
function TeamCityReporter.report(results)
local resultBuffer = {
"Test results:",
report(results),
("%d passed, %d failed, %d skipped"):format(
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:")
print("")
for _, message in ipairs(results.errors) do
TestService:Error(message)
-- Insert a blank line after each error
print("")
end
end
end
return TeamCityReporter

View file

@ -0,0 +1,106 @@
--[[
The TextReporter uses the results from a completed test to output text to
standard output and TestService.
]]
local TestService = game:GetService("TestService")
local TestEnum = require(script.Parent.Parent.TestEnum)
local INDENT = (" "):rep(3)
local STATUS_SYMBOLS = {
[TestEnum.TestStatus.Success] = "+",
[TestEnum.TestStatus.Failure] = "-",
[TestEnum.TestStatus.Skipped] = "~"
}
local UNKNOWN_STATUS_SYMBOL = "?"
local TextReporter = {}
local function compareNodes(a, b)
return a.planNode.phrase:lower() < b.planNode.phrase:lower()
end
local function reportNode(node, buffer, level)
buffer = buffer or {}
level = level or 0
if node.status == TestEnum.TestStatus.Skipped then
return buffer
end
local line
if node.status then
local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
line = ("%s[%s] %s"):format(
INDENT:rep(level),
symbol,
node.planNode.phrase
)
else
line = ("%s%s"):format(
INDENT:rep(level),
node.planNode.phrase
)
end
table.insert(buffer, line)
table.sort(node.children, compareNodes)
for _, child in ipairs(node.children) do
reportNode(child, buffer, level + 1)
end
return buffer
end
local function reportRoot(node)
local buffer = {}
table.sort(node.children, compareNodes)
for _, child in ipairs(node.children) do
reportNode(child, buffer, 0)
end
return buffer
end
local function report(root)
local buffer = reportRoot(root)
return table.concat(buffer, "\n")
end
function TextReporter.report(results)
local resultBuffer = {
"Test results:",
report(results),
("%d passed, %d failed, %d skipped"):format(
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:")
print("")
for _, message in ipairs(results.errors) do
TestService:Error(message)
-- Insert a blank line after each error
print("")
end
end
end
return TextReporter

View file

@ -0,0 +1,97 @@
--[[
Copy of TextReporter that doesn't output successful tests.
This should be temporary, it's just a workaround to make CI environments
happy in the short-term.
]]
local TestService = game:GetService("TestService")
local TestEnum = require(script.Parent.Parent.TestEnum)
local INDENT = (" "):rep(3)
local STATUS_SYMBOLS = {
[TestEnum.TestStatus.Success] = "+",
[TestEnum.TestStatus.Failure] = "-",
[TestEnum.TestStatus.Skipped] = "~"
}
local UNKNOWN_STATUS_SYMBOL = "?"
local TextReporterQuiet = {}
local function reportNode(node, buffer, level)
buffer = buffer or {}
level = level or 0
if node.status == TestEnum.TestStatus.Skipped then
return buffer
end
local line
if node.status ~= TestEnum.TestStatus.Success then
local symbol = STATUS_SYMBOLS[node.status] or UNKNOWN_STATUS_SYMBOL
line = ("%s[%s] %s"):format(
INDENT:rep(level),
symbol,
node.planNode.phrase
)
end
table.insert(buffer, line)
for _, child in ipairs(node.children) do
reportNode(child, buffer, level + 1)
end
return buffer
end
local function reportRoot(node)
local buffer = {}
for _, child in ipairs(node.children) do
reportNode(child, buffer, 0)
end
return buffer
end
local function report(root)
local buffer = reportRoot(root)
return table.concat(buffer, "\n")
end
function TextReporterQuiet.report(results)
local resultBuffer = {
"Test results:",
report(results),
("%d passed, %d failed, %d skipped"):format(
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:")
print("")
for _, message in ipairs(results.errors) do
TestService:Error(message)
-- Insert a blank line after each error
print("")
end
end
end
return TextReporterQuiet

147
TestEZ/TestBootstrap.lua Normal file
View file

@ -0,0 +1,147 @@
--[[
Provides an interface to quickly run and report tests from a given object.
]]
local TestPlanner = require(script.Parent.TestPlanner)
local TestRunner = require(script.Parent.TestRunner)
local TextReporter = require(script.Parent.Reporters.TextReporter)
local TestBootstrap = {}
local function stripSpecSuffix(name)
return (name:gsub("%.spec$", ""))
end
local function isSpecScript(aScript)
return aScript:IsA("ModuleScript") and aScript.Name:match("%.spec$")
end
local function getPath(module, root)
root = root or game
local path = {}
local last = module
if last.Name == "init.spec" then
-- Use the directory's node for init.spec files.
last = last.Parent
end
while last ~= nil and last ~= root do
table.insert(path, stripSpecSuffix(last.Name))
last = last.Parent
end
table.insert(path, stripSpecSuffix(root.Name))
return path
end
local function toStringPath(tablePath)
local stringPath = ""
local first = true
for _, element in ipairs(tablePath) do
if first then
stringPath = element
first = false
else
stringPath = element .. " " .. stringPath
end
end
return stringPath
end
function TestBootstrap:getModulesImpl(root, modules, current)
modules = modules or {}
current = current or root
if isSpecScript(current) then
local method = require(current)
local path = getPath(current, root)
local pathString = toStringPath(path)
table.insert(modules, {
method = method,
path = path,
pathStringForSorting = pathString:lower()
})
end
end
--[[
Find all the ModuleScripts in this tree that are tests.
]]
function TestBootstrap:getModules(root)
local modules = {}
self:getModulesImpl(root, modules)
for _, child in ipairs(root:GetDescendants()) do
self:getModulesImpl(root, modules, child)
end
return modules
end
--[[
Runs all test and reports the results using the given test reporter.
If no reporter is specified, a reasonable default is provided.
This function demonstrates the expected workflow with this testing system:
1. Locate test modules
2. Generate test plan
3. Run test plan
4. Report test results
This means we could hypothetically present a GUI to the developer that shows
the test plan before we execute it, allowing them to toggle specific tests
before they're run, but after they've been identified!
]]
function TestBootstrap:run(roots, reporter, otherOptions)
reporter = reporter or TextReporter
otherOptions = otherOptions or {}
local showTimingInfo = otherOptions["showTimingInfo"] or false
local testNamePattern = otherOptions["testNamePattern"]
local extraEnvironment = otherOptions["extraEnvironment"] or {}
if type(roots) ~= "table" then
error(("Bad argument #1 to TestBootstrap:run. Expected table, got %s"):format(typeof(roots)), 2)
end
local startTime = tick()
local modules = {}
for _, subRoot in ipairs(roots) do
local newModules = self:getModules(subRoot)
for _, newModule in ipairs(newModules) do
table.insert(modules, newModule)
end
end
local afterModules = tick()
local plan = TestPlanner.createPlan(modules, testNamePattern, extraEnvironment)
local afterPlan = tick()
local results = TestRunner.runPlan(plan)
local afterRun = tick()
reporter.report(results)
local afterReport = tick()
if showTimingInfo then
local timing = {
("Took %f seconds to locate test modules"):format(afterModules - startTime),
("Took %f seconds to create test plan"):format(afterPlan - afterModules),
("Took %f seconds to run tests"):format(afterRun - afterPlan),
("Took %f seconds to report tests"):format(afterReport - afterRun),
}
print(table.concat(timing, "\n"))
end
return results
end
return TestBootstrap

28
TestEZ/TestEnum.lua Normal file
View file

@ -0,0 +1,28 @@
--[[
Constants used throughout the testing framework.
]]
local TestEnum = {}
TestEnum.TestStatus = {
Success = "Success",
Failure = "Failure",
Skipped = "Skipped"
}
TestEnum.NodeType = {
Describe = "Describe",
It = "It",
BeforeAll = "BeforeAll",
AfterAll = "AfterAll",
BeforeEach = "BeforeEach",
AfterEach = "AfterEach"
}
TestEnum.NodeModifier = {
None = "None",
Skip = "Skip",
Focus = "Focus"
}
return TestEnum

304
TestEZ/TestPlan.lua Normal file
View file

@ -0,0 +1,304 @@
--[[
Represents a tree of tests that have been loaded but not necessarily
executed yet.
TestPlan objects are produced by TestPlanner.
]]
local TestEnum = require(script.Parent.TestEnum)
local Expectation = require(script.Parent.Expectation)
local function newEnvironment(currentNode, extraEnvironment)
local env = {}
if extraEnvironment then
if type(extraEnvironment) ~= "table" then
error(("Bad argument #2 to newEnvironment. Expected table, got %s"):format(
typeof(extraEnvironment)), 2)
end
for key, value in pairs(extraEnvironment) do
env[key] = value
end
end
local function addChild(phrase, callback, nodeType, nodeModifier)
local node = currentNode:addChild(phrase, nodeType, nodeModifier)
node.callback = callback
if nodeType == TestEnum.NodeType.Describe then
node:expand()
end
return node
end
function env.describeFOCUS(phrase, callback)
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Focus)
end
function env.describeSKIP(phrase, callback)
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.Skip)
end
function env.describe(phrase, callback, nodeModifier)
addChild(phrase, callback, TestEnum.NodeType.Describe, TestEnum.NodeModifier.None)
end
function env.itFOCUS(phrase, callback)
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Focus)
end
function env.itSKIP(phrase, callback)
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
end
function env.itFIXME(phrase, callback)
local node = addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.Skip)
warn("FIXME: broken test", node:getFullName())
end
function env.it(phrase, callback, nodeModifier)
addChild(phrase, callback, TestEnum.NodeType.It, TestEnum.NodeModifier.None)
end
-- Incrementing counter used to ensure that beforeAll, afterAll, beforeEach, afterEach have unique phrases
local lifecyclePhaseId = 0
local lifecycleHooks = {
[TestEnum.NodeType.BeforeAll] = "beforeAll",
[TestEnum.NodeType.AfterAll] = "afterAll",
[TestEnum.NodeType.BeforeEach] = "beforeEach",
[TestEnum.NodeType.AfterEach] = "afterEach"
}
for nodeType, name in pairs(lifecycleHooks) do
env[name] = function(callback)
addChild(name .. "_" .. tostring(lifecyclePhaseId), callback, nodeType, TestEnum.NodeModifier.None)
lifecyclePhaseId = lifecyclePhaseId + 1
end
end
function env.FIXME(optionalMessage)
warn("FIXME: broken test", currentNode:getFullName(), optionalMessage or "")
currentNode.modifier = TestEnum.NodeModifier.Skip
end
function env.FOCUS()
currentNode.modifier = TestEnum.NodeModifier.Focus
end
function env.SKIP()
currentNode.modifier = TestEnum.NodeModifier.Skip
end
--[[
This function is deprecated. Calling it is a no-op beyond generating a
warning.
]]
function env.HACK_NO_XPCALL()
warn("HACK_NO_XPCALL is deprecated. It is now safe to yield in an " ..
"xpcall, so this is no longer necessary. It can be safely deleted.")
end
env.fit = env.itFOCUS
env.xit = env.itSKIP
env.fdescribe = env.describeFOCUS
env.xdescribe = env.describeSKIP
env.expect = setmetatable({
extend = function(...)
error("Cannot call \"expect.extend\" from within a \"describe\" node.")
end,
}, {
__call = function(_self, ...)
return Expectation.new(...)
end,
})
return env
end
local TestNode = {}
TestNode.__index = TestNode
--[[
Create a new test node. A pointer to the test plan, a phrase to describe it
and the type of node it is are required. The modifier is optional and will
be None if left blank.
]]
function TestNode.new(plan, phrase, nodeType, nodeModifier)
nodeModifier = nodeModifier or TestEnum.NodeModifier.None
local node = {
plan = plan,
phrase = phrase,
type = nodeType,
modifier = nodeModifier,
children = {},
callback = nil,
parent = nil,
}
node.environment = newEnvironment(node, plan.extraEnvironment)
return setmetatable(node, TestNode)
end
local function getModifier(name, pattern, modifier)
if pattern and (modifier == nil or modifier == TestEnum.NodeModifier.None) then
if name:match(pattern) then
return TestEnum.NodeModifier.Focus
else
return TestEnum.NodeModifier.Skip
end
end
return modifier
end
function TestNode:addChild(phrase, nodeType, nodeModifier)
if nodeType == TestEnum.NodeType.It then
for _, child in pairs(self.children) do
if child.phrase == phrase then
error("Duplicate it block found: " .. child:getFullName())
end
end
end
local childName = self:getFullName() .. " " .. phrase
nodeModifier = getModifier(childName, self.plan.testNamePattern, nodeModifier)
local child = TestNode.new(self.plan, phrase, nodeType, nodeModifier)
child.parent = self
table.insert(self.children, child)
return child
end
--[[
Join the names of all the nodes back to the parent.
]]
function TestNode:getFullName()
if self.parent then
local parentPhrase = self.parent:getFullName()
if parentPhrase then
return parentPhrase .. " " .. self.phrase
end
end
return self.phrase
end
--[[
Expand a node by setting its callback environment and then calling it. Any
further it and describe calls within the callback will be added to the tree.
]]
function TestNode:expand()
local originalEnv = getfenv(self.callback)
local callbackEnv = setmetatable({}, { __index = originalEnv })
for key, value in pairs(self.environment) do
callbackEnv[key] = value
end
-- Copy 'script' directly to new env to make Studio debugger happy.
-- Studio debugger does not look into __index, because of security reasons
callbackEnv.script = originalEnv.script
setfenv(self.callback, callbackEnv)
local success, result = xpcall(self.callback, function(message)
return debug.traceback(tostring(message), 2)
end)
if not success then
self.loadError = result
end
end
local TestPlan = {}
TestPlan.__index = TestPlan
--[[
Create a new, empty TestPlan.
]]
function TestPlan.new(testNamePattern, extraEnvironment)
local plan = {
children = {},
testNamePattern = testNamePattern,
extraEnvironment = extraEnvironment,
}
return setmetatable(plan, TestPlan)
end
--[[
Add a new child under the test plan's root node.
]]
function TestPlan:addChild(phrase, nodeType, nodeModifier)
nodeModifier = getModifier(phrase, self.testNamePattern, nodeModifier)
local child = TestNode.new(self, phrase, nodeType, nodeModifier)
table.insert(self.children, child)
return child
end
--[[
Add a new describe node with the given method as a callback. Generates or
reuses all the describe nodes along the path.
]]
function TestPlan:addRoot(path, method)
local curNode = self
for i = #path, 1, -1 do
local nextNode = nil
for _, child in ipairs(curNode.children) do
if child.phrase == path[i] then
nextNode = child
break
end
end
if nextNode == nil then
nextNode = curNode:addChild(path[i], TestEnum.NodeType.Describe)
end
curNode = nextNode
end
curNode.callback = method
curNode:expand()
end
--[[
Calls the given callback on all nodes in the tree, traversed depth-first.
]]
function TestPlan:visitAllNodes(callback, root, level)
root = root or self
level = level or 0
for _, child in ipairs(root.children) do
callback(child, level)
self:visitAllNodes(callback, child, level + 1)
end
end
--[[
Visualizes the test plan in a simple format, suitable for debugging the test
plan's structure.
]]
function TestPlan:visualize()
local buffer = {}
self:visitAllNodes(function(node, level)
table.insert(buffer, (" "):rep(3 * level) .. node.phrase)
end)
return table.concat(buffer, "\n")
end
--[[
Gets a list of all nodes in the tree for which the given callback returns
true.
]]
function TestPlan:findNodes(callback)
local results = {}
self:visitAllNodes(function(node)
if callback(node) then
table.insert(results, node)
end
end)
return results
end
return TestPlan

40
TestEZ/TestPlanner.lua Normal file
View file

@ -0,0 +1,40 @@
--[[
Turns a series of specification functions into a test plan.
Uses a TestPlanBuilder to keep track of the state of the tree being built.
]]
local TestPlan = require(script.Parent.TestPlan)
local TestPlanner = {}
--[[
Create a new TestPlan from a list of specification functions.
These functions should call a combination of `describe` and `it` (and their
variants), which will be turned into a test plan to be executed.
Parameters:
- modulesList - list of tables describing test modules {
method, -- specification function described above
path, -- array of parent entires, first element is the leaf that owns `method`
pathStringForSorting -- a string representation of `path`, used for sorting of the test plan
}
- testNamePattern - Only tests matching this Lua pattern string will run. Pass empty or nil to run all tests
- extraEnvironment - Lua table holding additional functions and variables to be injected into the specification
function during execution
]]
function TestPlanner.createPlan(modulesList, testNamePattern, extraEnvironment)
local plan = TestPlan.new(testNamePattern, extraEnvironment)
table.sort(modulesList, function(a, b)
return a.pathStringForSorting < b.pathStringForSorting
end)
for _, module in ipairs(modulesList) do
plan:addRoot(module.path, module.method)
end
return plan
end
return TestPlanner

112
TestEZ/TestResults.lua Normal file
View file

@ -0,0 +1,112 @@
--[[
Represents a tree of test results.
Each node in the tree corresponds directly to a node in a corresponding
TestPlan, accessible via the 'planNode' field.
TestResults objects are produced by TestRunner using TestSession as state.
]]
local TestEnum = require(script.Parent.TestEnum)
local STATUS_SYMBOLS = {
[TestEnum.TestStatus.Success] = "+",
[TestEnum.TestStatus.Failure] = "-",
[TestEnum.TestStatus.Skipped] = "~"
}
local TestResults = {}
TestResults.__index = TestResults
--[[
Create a new TestResults tree that's linked to the given TestPlan.
]]
function TestResults.new(plan)
local self = {
successCount = 0,
failureCount = 0,
skippedCount = 0,
planNode = plan,
children = {},
errors = {}
}
setmetatable(self, TestResults)
return self
end
--[[
Create a new result node that can be inserted into a TestResult tree.
]]
function TestResults.createNode(planNode)
local node = {
planNode = planNode,
children = {},
errors = {},
status = nil
}
return node
end
--[[
Visit all test result nodes, depth-first.
]]
function TestResults:visitAllNodes(callback, root)
root = root or self
for _, child in ipairs(root.children) do
callback(child)
self:visitAllNodes(callback, child)
end
end
--[[
Creates a debug visualization of the test results.
]]
function TestResults:visualize(root, level)
root = root or self
level = level or 0
local buffer = {}
for _, child in ipairs(root.children) do
if child.planNode.type == TestEnum.NodeType.It then
local symbol = STATUS_SYMBOLS[child.status] or "?"
local str = ("%s[%s] %s"):format(
(" "):rep(3 * level),
symbol,
child.planNode.phrase
)
if child.messages and #child.messages > 0 then
str = str .. "\n " .. (" "):rep(3 * level) .. table.concat(child.messages, "\n " .. (" "):rep(3 * level))
end
table.insert(buffer, str)
else
local str = ("%s%s"):format(
(" "):rep(3 * level),
child.planNode.phrase or ""
)
if child.status then
str = str .. (" (%s)"):format(child.status)
end
table.insert(buffer, str)
if #child.children > 0 then
local text = self:visualize(child, level + 1)
table.insert(buffer, text)
end
end
end
return table.concat(buffer, "\n")
end
return TestResults

188
TestEZ/TestRunner.lua Normal file
View file

@ -0,0 +1,188 @@
--[[
Contains the logic to run a test plan and gather test results from it.
TestRunner accepts a TestPlan object, executes the planned tests, and
produces a TestResults object. While the tests are running, the system's
state is contained inside a TestSession object.
]]
local TestEnum = require(script.Parent.TestEnum)
local TestSession = require(script.Parent.TestSession)
local LifecycleHooks = require(script.Parent.LifecycleHooks)
local RUNNING_GLOBAL = "__TESTEZ_RUNNING_TEST__"
local TestRunner = {
environment = {}
}
local function wrapExpectContextWithPublicApi(expectationContext)
return setmetatable({
extend = function(...)
expectationContext:extend(...)
end,
}, {
__call = function(_self, ...)
return expectationContext:startExpectationChain(...)
end,
})
end
--[[
Runs the given TestPlan and returns a TestResults object representing the
results of the run.
]]
function TestRunner.runPlan(plan)
local session = TestSession.new(plan)
local lifecycleHooks = LifecycleHooks.new()
local exclusiveNodes = plan:findNodes(function(node)
return node.modifier == TestEnum.NodeModifier.Focus
end)
session.hasFocusNodes = #exclusiveNodes > 0
TestRunner.runPlanNode(session, plan, lifecycleHooks)
return session:finalize()
end
--[[
Run the given test plan node and its descendants, using the given test
session to store all of the results.
]]
function TestRunner.runPlanNode(session, planNode, lifecycleHooks)
local function runCallback(callback, messagePrefix)
local success = true
local errorMessage
-- Any code can check RUNNING_GLOBAL to fork behavior based on
-- whether a test is running. We use this to avoid accessing
-- protected APIs; it's a workaround that will go away someday.
_G[RUNNING_GLOBAL] = true
messagePrefix = messagePrefix or ""
local testEnvironment = getfenv(callback)
for key, value in pairs(TestRunner.environment) do
testEnvironment[key] = value
end
testEnvironment.fail = function(message)
if message == nil then
message = "fail() was called."
end
success = false
errorMessage = messagePrefix .. debug.traceback(tostring(message), 2)
end
testEnvironment.expect = wrapExpectContextWithPublicApi(session:getExpectationContext())
local context = session:getContext()
local nodeSuccess, nodeResult = xpcall(
function()
callback(context)
end,
function(message)
return messagePrefix .. debug.traceback(tostring(message), 2)
end
)
-- If a node threw an error, we prefer to use that message over
-- one created by fail() if it was set.
if not nodeSuccess then
success = false
errorMessage = nodeResult
end
_G[RUNNING_GLOBAL] = nil
return success, errorMessage
end
local function runNode(childPlanNode)
-- Errors can be set either via `error` propagating upwards or
-- by a test calling fail([message]).
for _, hook in ipairs(lifecycleHooks:getBeforeEachHooks()) do
local success, errorMessage = runCallback(hook, "beforeEach hook: ")
if not success then
return false, errorMessage
end
end
local testSuccess, testErrorMessage = runCallback(childPlanNode.callback)
for _, hook in ipairs(lifecycleHooks:getAfterEachHooks()) do
local success, errorMessage = runCallback(hook, "afterEach hook: ")
if not success then
if not testSuccess then
return false, testErrorMessage .. "\nWhile cleaning up the failed test another error was found:\n" .. errorMessage
end
return false, errorMessage
end
end
if not testSuccess then
return false, testErrorMessage
end
return true, nil
end
lifecycleHooks:pushHooksFrom(planNode)
local halt = false
for _, hook in ipairs(lifecycleHooks:getBeforeAllHooks()) do
local success, errorMessage = runCallback(hook, "beforeAll hook: ")
if not success then
session:addDummyError("beforeAll", errorMessage)
halt = true
end
end
if not halt then
for _, childPlanNode in ipairs(planNode.children) do
if childPlanNode.type == TestEnum.NodeType.It then
session:pushNode(childPlanNode)
if session:shouldSkip() then
session:setSkipped()
else
local success, errorMessage = runNode(childPlanNode)
if success then
session:setSuccess()
else
session:setError(errorMessage)
end
end
session:popNode()
elseif childPlanNode.type == TestEnum.NodeType.Describe then
session:pushNode(childPlanNode)
TestRunner.runPlanNode(session, childPlanNode, lifecycleHooks)
-- Did we have an error trying build a test plan?
if childPlanNode.loadError then
local message = "Error during planning: " .. childPlanNode.loadError
session:setError(message)
else
session:setStatusFromChildren()
end
session:popNode()
end
end
end
for _, hook in ipairs(lifecycleHooks:getAfterAllHooks()) do
local success, errorMessage = runCallback(hook, "afterAll hook: ")
if not success then
session:addDummyError("afterAll", errorMessage)
end
end
lifecycleHooks:popHooks()
end
return TestRunner

243
TestEZ/TestSession.lua Normal file
View file

@ -0,0 +1,243 @@
--[[
Represents the state relevant while executing a test plan.
Used by TestRunner to produce a TestResults object.
Uses the same tree building structure as TestPlanBuilder; TestSession keeps
track of a stack of nodes that represent the current path through the tree.
]]
local TestEnum = require(script.Parent.TestEnum)
local TestResults = require(script.Parent.TestResults)
local Context = require(script.Parent.Context)
local ExpectationContext = require(script.Parent.ExpectationContext)
local TestSession = {}
TestSession.__index = TestSession
--[[
Create a TestSession related to the given TestPlan.
The resulting TestResults object will be linked to this TestPlan.
]]
function TestSession.new(plan)
local self = {
results = TestResults.new(plan),
nodeStack = {},
contextStack = {},
expectationContextStack = {},
hasFocusNodes = false
}
setmetatable(self, TestSession)
return self
end
--[[
Calculate success, failure, and skipped test counts in the tree at the
current point in the execution.
]]
function TestSession:calculateTotals()
local results = self.results
results.successCount = 0
results.failureCount = 0
results.skippedCount = 0
results:visitAllNodes(function(node)
local status = node.status
local nodeType = node.planNode.type
if nodeType == TestEnum.NodeType.It then
if status == TestEnum.TestStatus.Success then
results.successCount = results.successCount + 1
elseif status == TestEnum.TestStatus.Failure then
results.failureCount = results.failureCount + 1
elseif status == TestEnum.TestStatus.Skipped then
results.skippedCount = results.skippedCount + 1
end
end
end)
end
--[[
Gathers all of the errors reported by tests and puts them at the top level
of the TestResults object.
]]
function TestSession:gatherErrors()
local results = self.results
results.errors = {}
results:visitAllNodes(function(node)
if #node.errors > 0 then
for _, message in ipairs(node.errors) do
table.insert(results.errors, message)
end
end
end)
end
--[[
Calculates test totals, verifies the tree is valid, and returns results.
]]
function TestSession:finalize()
if #self.nodeStack ~= 0 then
error("Cannot finalize TestResults with nodes still on the stack!", 2)
end
self:calculateTotals()
self:gatherErrors()
return self.results
end
--[[
Create a new test result node and push it onto the navigation stack.
]]
function TestSession:pushNode(planNode)
local node = TestResults.createNode(planNode)
local lastNode = self.nodeStack[#self.nodeStack] or self.results
table.insert(lastNode.children, node)
table.insert(self.nodeStack, node)
local lastContext = self.contextStack[#self.contextStack]
local context = Context.new(lastContext)
table.insert(self.contextStack, context)
local lastExpectationContext = self.expectationContextStack[#self.expectationContextStack]
local expectationContext = ExpectationContext.new(lastExpectationContext)
table.insert(self.expectationContextStack, expectationContext)
end
--[[
Pops a node off of the navigation stack.
]]
function TestSession:popNode()
assert(#self.nodeStack > 0, "Tried to pop from an empty node stack!")
table.remove(self.nodeStack, #self.nodeStack)
table.remove(self.contextStack, #self.contextStack)
table.remove(self.expectationContextStack, #self.expectationContextStack)
end
--[[
Gets the Context object for the current node.
]]
function TestSession:getContext()
assert(#self.contextStack > 0, "Tried to get context from an empty stack!")
return self.contextStack[#self.contextStack]
end
function TestSession:getExpectationContext()
assert(#self.expectationContextStack > 0, "Tried to get expectationContext from an empty stack!")
return self.expectationContextStack[#self.expectationContextStack]
end
--[[
Tells whether the current test we're in should be skipped.
]]
function TestSession:shouldSkip()
-- If our test tree had any exclusive tests, then normal tests are skipped!
if self.hasFocusNodes then
for i = #self.nodeStack, 1, -1 do
local node = self.nodeStack[i]
-- Skipped tests are still skipped
if node.planNode.modifier == TestEnum.NodeModifier.Skip then
return true
end
-- Focused tests are the only ones that aren't skipped
if node.planNode.modifier == TestEnum.NodeModifier.Focus then
return false
end
end
return true
else
for i = #self.nodeStack, 1, -1 do
local node = self.nodeStack[i]
if node.planNode.modifier == TestEnum.NodeModifier.Skip then
return true
end
end
end
return false
end
--[[
Set the current node's status to Success.
]]
function TestSession:setSuccess()
assert(#self.nodeStack > 0, "Attempting to set success status on empty stack")
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Success
end
--[[
Set the current node's status to Skipped.
]]
function TestSession:setSkipped()
assert(#self.nodeStack > 0, "Attempting to set skipped status on empty stack")
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Skipped
end
--[[
Set the current node's status to Failure and adds a message to its list of
errors.
]]
function TestSession:setError(message)
assert(#self.nodeStack > 0, "Attempting to set error status on empty stack")
local last = self.nodeStack[#self.nodeStack]
last.status = TestEnum.TestStatus.Failure
table.insert(last.errors, message)
end
--[[
Add a dummy child node to the current node to hold the given error. This
allows an otherwise empty describe node to report an error in a more natural
way.
]]
function TestSession:addDummyError(phrase, message)
self:pushNode({type = TestEnum.NodeType.It, phrase = phrase})
self:setError(message)
self:popNode()
self.nodeStack[#self.nodeStack].status = TestEnum.TestStatus.Failure
end
--[[
Set the current node's status based on that of its children. If all children
are skipped, mark it as skipped. If any are fails, mark it as failed.
Otherwise, mark it as success.
]]
function TestSession:setStatusFromChildren()
assert(#self.nodeStack > 0, "Attempting to set status from children on empty stack")
local last = self.nodeStack[#self.nodeStack]
local status = TestEnum.TestStatus.Success
local skipped = true
-- If all children were skipped, then we were skipped
-- If any child failed, then we failed!
for _, child in ipairs(last.children) do
if child.status ~= TestEnum.TestStatus.Skipped then
skipped = false
if child.status == TestEnum.TestStatus.Failure then
status = TestEnum.TestStatus.Failure
end
end
end
if skipped then
status = TestEnum.TestStatus.Skipped
end
last.status = status
end
return TestSession

42
TestEZ/init.lua Normal file
View file

@ -0,0 +1,42 @@
--!native
--!optimize 2
local Expectation = require(script.Expectation)
local TestBootstrap = require(script.TestBootstrap)
local TestEnum = require(script.TestEnum)
local TestPlan = require(script.TestPlan)
local TestPlanner = require(script.TestPlanner)
local TestResults = require(script.TestResults)
local TestRunner = require(script.TestRunner)
local TestSession = require(script.TestSession)
local TextReporter = require(script.Reporters.TextReporter)
local TextReporterQuiet = require(script.Reporters.TextReporterQuiet)
local TeamCityReporter = require(script.Reporters.TeamCityReporter)
local function run(testRoot, callback)
local modules = TestBootstrap:getModules(testRoot)
local plan = TestPlanner.createPlan(modules)
local results = TestRunner.runPlan(plan)
callback(results)
end
local TestEZ = {
run = run,
Expectation = Expectation,
TestBootstrap = TestBootstrap,
TestEnum = TestEnum,
TestPlan = TestPlan,
TestPlanner = TestPlanner,
TestResults = TestResults,
TestRunner = TestRunner,
TestSession = TestSession,
Reporters = {
TextReporter = TextReporter,
TextReporterQuiet = TextReporterQuiet,
TeamCityReporter = TeamCityReporter,
},
}
return TestEZ

BIN
Warp.rbxm

Binary file not shown.

View file

@ -3,5 +3,5 @@
# To add a new tool, add an entry to this table. # To add a new tool, add an entry to this table.
[tools] [tools]
rojo = "rojo-rbx/rojo@7.4.0" rojo = "rojo-rbx/rojo@7.4.1"
wally = "UpliftGames/wally@0.3.2" wally = "UpliftGames/wally@0.3.2"

View file

@ -87,7 +87,7 @@ Disconnect the event connection.
```lua [Variable] ```lua [Variable]
( (
key: string key: string
) ): boolean
``` ```
```lua [Example] ```lua [Example]
@ -128,7 +128,7 @@ This function have rate limiting it self and configured from server.
## `:Invoke` <Badge type="warning" text="yield" /> ## `:Invoke` <Badge type="warning" text="yield" />
Semiliar to `:InvokeServer`, its for Invoke to a server. Semiliar to `:InvokeServer`, but it have timeout system that not exists on `RemoteFunction.InvokeServer`.
::: code-group ::: code-group
```lua [Variable] ```lua [Variable]
@ -139,7 +139,7 @@ Semiliar to `:InvokeServer`, its for Invoke to a server.
``` ```
```lua [Example] ```lua [Example]
local Request = Remote:Invoke(2, "Hello World!") local Request = Remote:Invoke(2, "Hello World!") -- this yield until it response
``` ```
::: :::
@ -152,7 +152,7 @@ This function is yielded, once it timeout it will return nil.
Wait the event being triggered. Wait the event being triggered.
```lua ```lua
Remote:Wait() Remote:Wait() -- :Wait return number value
``` ```
::: warning ::: warning
@ -161,7 +161,7 @@ This function is yielded, Invoke might also ping this one and also causing error
## `:Destroy` ## `:Destroy`
Disconnect all connection of event and remove the event from Warp Disconnect all connection of event and remove the event from Warp list
```lua ```lua
Remote:Destroy() Remote:Destroy()

View file

@ -13,8 +13,10 @@ When creating a event on Server, you can add second argument (optional) as table
-- Server -- Server
-- Let's make the event have ratelimit with max 50 entrance for 2 seconds. -- Let's make the event have ratelimit with max 50 entrance for 2 seconds.
local Remote = Warp.Server("Remote1", { local Remote = Warp.Server("Remote1", {
rateLimit = {
maxEntrance = 50, -- maximum 50 fires. maxEntrance = 50, -- maximum 50 fires.
interval = 2, -- 2 seconds interval = 2, -- 2 seconds
}
}) })
-- Now the Event RateLimit is configured, and ready to use. -- Now the Event RateLimit is configured, and ready to use.
-- No need anything to adds on client side. -- No need anything to adds on client side.

View file

@ -36,12 +36,16 @@ Create new Warp events with array.
```lua [Example] ```lua [Example]
local Events = Warp.fromServerArray({ local Events = Warp.fromServerArray({
["Remote1"] = { ["Remote1"] = {
rateLimit = {
maxEntrance: 50, maxEntrance: 50,
interval: 1, interval: 1,
}
}, -- with rateLimit configuration }, -- with rateLimit configuration
"Remote2", -- without rateLimit configuration "Remote2", -- without rateLimit configuration
["Remote3"] = { ["Remote3"] = {
rateLimit = {
maxEntrance: 10, maxEntrance: 10,
}
}, -- with rateLimit configuration }, -- with rateLimit configuration
}) })
@ -97,7 +101,7 @@ Disconnect the event connection.
```lua [Variable] ```lua [Variable]
( (
key: string key: string
) ): boolean
``` ```
```lua [Example] ```lua [Example]
@ -150,9 +154,27 @@ Remote:Fires(true, "Hello World!")
``` ```
::: :::
## `:FireExcept` <Badge type="tip" text="Server Only" />
Fire the event to all clients but except a players.
::: code-group
```lua [Variable]
(
reliable: boolean,
except: { Player },
...: any
)
```
```lua [Example]
Remote:FireExcept(true, { Players.Eternity_Devs, Players.Player2 }, "Hello World!") -- this will sent to all players except { Players.Eternity_Devs, Players.Player2 }.
```
:::
## `:Invoke` <Badge type="warning" text="yield" /> ## `:Invoke` <Badge type="warning" text="yield" />
Semiliar to `:InvokeClient`, its for Invoke to a client. Semiliar to `:InvokeClient`, but it have timeout system that not exists on `RemoteFunction.InvokeClient`.
::: code-group ::: code-group
```lua [Variable] ```lua [Variable]
@ -177,7 +199,7 @@ This function is yielded, once it timeout it will return nil.
Wait the event being triggered. Wait the event being triggered.
```lua ```lua
Remote:Wait() Remote:Wait() -- :Wait return number value
``` ```
::: warning ::: warning

View file

@ -102,7 +102,23 @@ Signal1:DisconnectAll()
## `:Fire` ## `:Fire`
Fire the signal. Fire the signal (Immediate)
::: code-group
```lua [Variable]
(
...: any
)
```
```lua [Example]
Signal1:Fire("Hello World!")
```
:::
## `:DeferFire`
Fire the signal (Deferred)
::: code-group ::: code-group
```lua [Variable] ```lua [Variable]
@ -122,7 +138,7 @@ This uses `pcall`, which means it never error (safe-mode, sacrificed debugging),
## `:FireTo` ## `:FireTo`
Fire to other signal, this also use `:Fire`. Fire to other signal, this uses `:Fire`.
::: code-group ::: code-group
```lua [Variable] ```lua [Variable]

View file

@ -68,3 +68,4 @@ Pong:Destroy()
-- Yay Done! -- Yay Done!
``` ```
:::

View file

@ -8,7 +8,7 @@
::: code-group ::: code-group
```toml [wally.toml] ```toml [wally.toml]
[dependencies] [dependencies]
warp = "imezx/warp@1.0.5" warp = "imezx/warp@1.0.13"
``` ```
3. Run `wally install` in command. 3. Run `wally install` in command.

3
runTests.server.luau Normal file
View file

@ -0,0 +1,3 @@
require(script.Parent.TestEZ).TestBootstrap:run({
game:GetService("ServerScriptService").Test
})

View file

@ -1,208 +0,0 @@
--!native
--!strict
local ClientProcess = {}
local RunService = game:GetService("RunService")
local Util = script.Parent.Parent.Util
local Type = require(script.Parent.Parent.Type)
local Event = require(script.Parent.Parent.Event)
local Spawn = require(Util.Spawn)
local Key = require(Util.Key)
local RateLimit = require(Util.RateLimit)
local clientRatelimit: Type.StoredRatelimit = {}
local clientQueue: Type.QueueMap = {}
local unreliableClientQueue: Type.QueueMap = {}
local clientCallback: Type.CallbackMap = {}
local clientRequestQueue: Type.QueueMap = {}
local queueIn: {
[string]: {any}
} = {}
local queueInRequest: {
[number]: {
[string]: {
any
}
}
} = {}
local queueOutRequest: {
[number]: {
[string]: {
any
}
}
} = {}
local incoming_cache: {
[string]: {
any
}
} = {}
queueInRequest[1] = {}
queueInRequest[2] = {}
queueOutRequest[1] = {}
queueOutRequest[2] = {}
local ReliableEvent = Event.Reliable
local UnreliableEvent = Event.Unreliable
local RequestEvent = Event.Request
function ClientProcess.insertQueue(Identifier: string, reliable: boolean, ...: any)
if not reliable then
table.insert(unreliableClientQueue[Identifier], { ... })
return
end
table.insert(clientQueue[Identifier], { ... })
end
function ClientProcess.insertRequest(Identifier: string, timeout: number, ...: any)
local yieldThread: thread, start = coroutine.running(), os.clock()
local cancel = task.delay(timeout, function()
task.spawn(yieldThread, nil)
end)
table.insert(clientRequestQueue[Identifier], { tostring(Key()), function(...: any)
if (os.clock() - start) > timeout then return end
task.cancel(cancel)
task.spawn(yieldThread, ...)
end :: any, { ... } :: any })
return coroutine.yield()
end
function ClientProcess.add(Identifier: string, originId: string)
if not clientQueue[Identifier] then
clientRatelimit[Identifier] = RateLimit.create(originId)
clientQueue[Identifier] = {}
unreliableClientQueue[Identifier] = {}
clientRequestQueue[Identifier] = {}
clientCallback[Identifier] = {}
queueOutRequest[1][Identifier] = {}
queueOutRequest[2][Identifier] = {}
queueInRequest[1][Identifier] = {}
queueInRequest[2][Identifier] = {}
queueIn[Identifier] = {}
end
end
function ClientProcess.addCallback(Identifier: string, key: string, callback)
clientCallback[Identifier][key] = callback
end
function ClientProcess.removeCallback(Identifier: string, key: string)
clientCallback[Identifier][key] = nil
end
function ClientProcess.start()
RunService.PostSimulation:Connect(function()
for Identifier: string, data: any in unreliableClientQueue do
if #data > 0 then
if clientRatelimit[Identifier](#data) then
UnreliableEvent:FireServer(Identifier, data)
end
table.clear(data)
end
end
for Identifier: string, data: any in clientQueue do
if #data > 0 then
if clientRatelimit[Identifier](#data) then
ReliableEvent:FireServer(Identifier, data)
end
table.clear(data)
end
if #clientRequestQueue[Identifier] > 0 then
for _, requestData in clientRequestQueue[Identifier] do
if not requestData[3] then continue end
table.insert(queueOutRequest[1][Identifier], { requestData[1], requestData[3] })
table.remove(requestData, #requestData)
end
end
if incoming_cache[Identifier] then
for _, packet in incoming_cache[Identifier] do
if not queueIn[Identifier] then continue end
table.insert(queueIn[Identifier], table.clone(packet))
table.clear(incoming_cache[Identifier])
end
end
if clientCallback[Identifier] then
if #queueIn[Identifier] > 0 then
for _, packedDatas: any in queueIn[Identifier] do
if #packedDatas == 0 then continue end
for _, v: any in packedDatas do
for _, fn: any in clientCallback[Identifier] do
Spawn(fn, table.unpack(v))
end
end
end
table.clear(queueIn[Identifier])
end
if #queueInRequest[1][Identifier] > 0 then
for idx, packetDatas: any in queueInRequest[1][Identifier] do
if #packetDatas == 0 then continue end
for _, packetData in packetDatas do
for _, fn: any in clientCallback[Identifier] do
Spawn(function()
local requestReturn = { fn(table.unpack(packetData[2])) }
table.insert(queueOutRequest[2][Identifier], { packetData[1], requestReturn })
end)
end
end
end
table.clear(queueInRequest[1][Identifier])
end
if #queueInRequest[2][Identifier] > 0 then
for _, packetDatas: any in queueInRequest[2][Identifier] do
for _, packetData in packetDatas do
if #packetData == 1 then continue end
for idx, clientRequest in clientRequestQueue[Identifier] do
if clientRequest[1] == packetData[1] then
Spawn(clientRequest[2], table.unpack(packetData[2]))
table.remove(clientRequestQueue[Identifier], idx)
break
end
end
end
end
table.clear(queueInRequest[2][Identifier])
end
end
end
for Identifier: string, requestsData in queueOutRequest[1] do
if #requestsData == 0 then continue end
RequestEvent:FireServer(Identifier, "\1", requestsData)
table.clear(queueOutRequest[1][Identifier])
end
for Identifier: string, requestsData in queueOutRequest[2] do
if #requestsData == 0 then continue end
RequestEvent:FireServer(Identifier, "\0", requestsData)
table.clear(queueOutRequest[2][Identifier])
end
end)
local function onClientNetworkReceive(Identifier: string, data: any)
if not Identifier or not data then return end
if not queueIn[Identifier] then
queueIn[Identifier] = {}
end
if not clientCallback[Identifier] then
if not incoming_cache[Identifier] then
incoming_cache[Identifier] = {}
end
table.insert(incoming_cache[Identifier], data)
return
end
table.insert(queueIn[Identifier], data)
end
ReliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
UnreliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
RequestEvent.OnClientEvent:Connect(function(Identifier: string, action: string, returnDatas)
if not Identifier or not returnDatas then return end
if action == "\1" then
table.insert(queueInRequest[1][Identifier], returnDatas)
else
table.insert(queueInRequest[2][Identifier], returnDatas)
end
end)
end
return ClientProcess

View file

@ -0,0 +1,264 @@
--!native
--!strict
--!optimize 2
local ClientProcess = {}
local RunService = game:GetService("RunService")
local Util = script.Parent.Parent.Util
local Type = require(script.Parent.Parent.Type)
local Event = require(script.Parent.Parent.Event)
local Spawn = require(Util.Spawn)
local Key = require(Util.Key)
local RateLimit = require(Util.RateLimit)
local Buffer = require(Util.Buffer)
local clientRatelimit: Type.StoredRatelimit = {}
local clientQueue: Type.QueueMap = {}
local unreliableClientQueue: Type.QueueMap = {}
local clientCallback: Type.CallbackMap = {}
local clientRequestQueue: Type.QueueMap = {}
local registeredIdentifier: { [string]: boolean } = {}
local queueInRequest: {
[number]: {
[string]: {
any
}
}
} = {}
local queueOutRequest: {
[number]: {
[string]: {
any
}
}
} = {}
queueInRequest[1] = {}
queueInRequest[2] = {}
queueOutRequest[1] = {}
queueOutRequest[2] = {}
local ReliableEvent = Event.Reliable
local UnreliableEvent = Event.Unreliable
local RequestEvent = Event.Request
function ClientProcess.insertQueue(Identifier: string, reliable: boolean, ...: any)
if not reliable then
if not unreliableClientQueue[Identifier] then
unreliableClientQueue[Identifier] = {}
end
table.insert(unreliableClientQueue[Identifier], { ... })
return
end
if not clientQueue[Identifier] then
clientQueue[Identifier] = {}
end
table.insert(clientQueue[Identifier], { ... })
end
function ClientProcess.insertRequest(Identifier: string, timeout: number, ...: any)
if not clientRequestQueue[Identifier] then
clientRequestQueue[Identifier] = {}
end
local yieldThread: thread, start = coroutine.running(), os.clock()
local cancel = task.delay(timeout, function()
task.spawn(yieldThread, nil)
end)
table.insert(clientRequestQueue[Identifier], { tostring(Key()), function(...: any)
if (os.clock() - start) > timeout then return end
task.cancel(cancel)
task.spawn(yieldThread, ...)
end :: any, { ... } :: any })
return coroutine.yield()
end
function ClientProcess.add(Identifier: any, originId: string, conf: Type.ClientConf)
if not registeredIdentifier[Identifier] then
registeredIdentifier[Identifier] = true
if not clientRatelimit[Identifier] then
clientRatelimit[Identifier] = RateLimit.create(originId)
end
if not clientQueue[Identifier] then
clientQueue[Identifier] = {}
end
if not unreliableClientQueue[Identifier] then
unreliableClientQueue[Identifier] = {}
end
if not clientRequestQueue[Identifier] then
clientRequestQueue[Identifier] = {}
end
if not clientCallback[Identifier] then
clientCallback[Identifier] = {}
end
if not queueOutRequest[1][Identifier] then
queueOutRequest[1][Identifier] = {}
end
if not queueOutRequest[2][Identifier] then
queueOutRequest[2][Identifier] = {}
end
if not queueInRequest[1][Identifier] then
queueInRequest[1][Identifier] = {}
end
if not queueInRequest[2][Identifier] then
queueInRequest[2][Identifier] = {}
end
end
end
function ClientProcess.remove(Identifier: string)
if not registeredIdentifier[Identifier] then return end
registeredIdentifier[Identifier] = nil
clientQueue[Identifier] = nil
unreliableClientQueue[Identifier] = nil
clientRequestQueue[Identifier] = nil
clientCallback[Identifier] = nil
clientRatelimit[Identifier] = nil
queueOutRequest[1][Identifier] = nil
queueOutRequest[2][Identifier] = nil
queueInRequest[1][Identifier] = nil
queueInRequest[2][Identifier] = nil
end
function ClientProcess.addCallback(Identifier: string, key: string, callback)
clientCallback[Identifier][key] = callback
end
function ClientProcess.removeCallback(Identifier: string, key: string)
clientCallback[Identifier][key] = nil
end
function ClientProcess.start()
debug.setmemorycategory("Warp")
RunService.PostSimulation:Connect(function()
-- Unreliable
for Identifier: string, data: any in unreliableClientQueue do
if #data == 0 then continue end
if clientRatelimit[Identifier](#data) then
for _, unpacked in data do
UnreliableEvent:FireServer(Buffer.revert(Identifier), Buffer.write(unpacked))
end
end
unreliableClientQueue[Identifier] = nil
end
-- Reliable
for Identifier: string, data: any in clientQueue do
if #data > 0 then
if clientRatelimit[Identifier](#data) then
for _, unpacked in data do
ReliableEvent:FireServer(Buffer.revert(Identifier), Buffer.write(unpacked))
end
end
clientQueue[Identifier] = nil
end
end
-- Sent new invokes
for Identifier: string, requestsData in queueOutRequest[1] do
if #requestsData == 0 then continue end
RequestEvent:FireServer(Buffer.revert(Identifier), "\1", requestsData)
queueOutRequest[1][Identifier] = nil
end
-- Sent returning invokes
for Identifier: string, toReturnDatas in queueOutRequest[2] do
if #toReturnDatas == 0 then continue end
RequestEvent:FireServer(Buffer.revert(Identifier), "\0", toReturnDatas)
queueOutRequest[2][Identifier] = nil
end
for Identifier: string in registeredIdentifier do
if clientRequestQueue[Identifier] then
for _, requestData in clientRequestQueue[Identifier] do
if not requestData[3] then continue end
if not queueOutRequest[1][Identifier] then
queueOutRequest[1][Identifier] = {}
end
table.insert(queueOutRequest[1][Identifier], { requestData[1], requestData[3] })
requestData[3] = nil
end
end
-- Unreliable & Reliable
local callback = clientCallback[Identifier] or nil
if not callback then continue end
-- Return Invoke
if queueInRequest[1][Identifier] then
for _, packetDatas: any in queueInRequest[1][Identifier] do
if #packetDatas == 0 then continue end
for _, fn: any in callback do
for i=1,#packetDatas do
if not packetDatas[i] then continue end
local packetData1 = packetDatas[i][1]
local packetData2 = packetDatas[i][2]
Spawn(function()
local requestReturn = { fn(table.unpack(packetData2)) }
if not queueOutRequest[2][Identifier] then
queueOutRequest[2][Identifier] = {}
end
table.insert(queueOutRequest[2][Identifier], { packetData1, requestReturn })
packetData1 = nil
packetData2 = nil
end)
end
end
end
queueInRequest[1][Identifier] = nil
end
-- Call to Invoke
if queueInRequest[2][Identifier] then
if clientRequestQueue[Identifier] then
for _, packetDatas: any in queueInRequest[2][Identifier] do
for _, packetData in packetDatas do
if #packetData == 1 then continue end
for y=1,#clientRequestQueue[Identifier] do
local clientRequest = clientRequestQueue[Identifier][y]
if not clientRequest then continue end
if clientRequest[1] == packetData[1] then
Spawn(clientRequest[2], table.unpack(packetData[2]))
table.remove(clientRequestQueue[Identifier], y)
break
end
end
end
end
end
queueInRequest[2][Identifier] = nil
end
end
end)
local function onClientNetworkReceive(Identifier: buffer | string, data: buffer, ref: { any }?)
if not Identifier or typeof(Identifier) ~= "buffer" or not data or typeof(data) ~= "buffer" then return end
Identifier = Buffer.convert(Identifier)
if not registeredIdentifier[Identifier :: string] then return end
local read = Buffer.read(data, ref)
if not read then return end
local callback = clientCallback[Identifier :: string]
if not callback then return end
for _, fn: any in callback do
Spawn(fn, table.unpack(read))
end
end
ReliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
UnreliableEvent.OnClientEvent:Connect(onClientNetworkReceive)
RequestEvent.OnClientEvent:Connect(function(Identifier: any, action: string, data)
if not Identifier or not data then return end
Identifier = Buffer.convert(Identifier)
if action == "\1" then
if not queueInRequest[1][Identifier] then
queueInRequest[1][Identifier] = {}
end
table.insert(queueInRequest[1][Identifier], data)
else
if not queueInRequest[2][Identifier] then
queueInRequest[2][Identifier] = {}
end
table.insert(queueInRequest[2][Identifier], data)
end
end)
end
return ClientProcess

View file

@ -1,20 +1,32 @@
--!strict --!strict
--!native
--!optimize 2
local Client = {} local Client = {}
Client.__index = Client Client.__index = Client
local Players = game:GetService("Players") local Players = game:GetService("Players")
local Util = script.Parent.Parent.Util local Util = script.Parent.Parent.Util
local Type = require(script.Parent.Parent.Type)
local ClientProcess = require(script.Parent.ClientProcess) local ClientProcess = require(script.Parent.ClientProcess)
local Assert = require(Util.Assert) local Assert = require(Util.Assert)
local Key = require(Util.Key) local Key = require(Util.Key)
local Serdes = require(Util.Serdes) local Serdes = require(Util.Serdes)
local Buffer = require(Util.Buffer)
function Client.new(Identifier: string) function Client.new(Identifier: string, conf: Type.ClientConf?)
local self = setmetatable({}, Client) local self = setmetatable({}, Client)
self.id = Serdes(Identifier)
self._buffer = Buffer.new()
self._buffer:wu8(Serdes.increment(Identifier, conf and conf.yieldWait))
self.id = Buffer.convert(self._buffer:build())
self.fn = {} self.fn = {}
ClientProcess.add(self.id, Identifier) self._conf = table.freeze(conf or {})
self.IsConnected = false
ClientProcess.add(self.id, Identifier, conf or { yieldWait = 10 })
self._buffer:remove()
return self return self
end end
@ -29,6 +41,7 @@ end
function Client:Connect(callback: (args: any) -> ()): string function Client:Connect(callback: (args: any) -> ()): string
local key = tostring(Key()) local key = tostring(Key())
table.insert(self.fn, key) table.insert(self.fn, key)
self.IsConnected = #self.fn > 0
ClientProcess.addCallback(self.id, key, callback) ClientProcess.addCallback(self.id, key, callback)
return key return key
end end
@ -36,7 +49,8 @@ end
function Client:Once(callback: (args: any) -> ()): string function Client:Once(callback: (args: any) -> ()): string
local key = tostring(Key()) local key = tostring(Key())
table.insert(self.fn, key) table.insert(self.fn, key)
ClientProcess.addCallback(self.id, key, function(...) self.IsConnected = #self.fn > 0
ClientProcess.addCallback(self.id, key, function(...: any?)
self:Disconnect(key) self:Disconnect(key)
task.spawn(callback, ...) task.spawn(callback, ...)
end) end)
@ -52,19 +66,24 @@ function Client:Wait()
end end
function Client:DisconnectAll() function Client:DisconnectAll()
for idx, key: string in self.fn do for _, key: string in self.fn do
ClientProcess.removeCallback(self.id, key) self:Disconnect(key)
table.remove(self.fn, idx)
end end
end end
function Client:Disconnect(key: string) function Client:Disconnect(key: string)
Assert(typeof(key) == "string", "Key must be a string type.") Assert(typeof(key) == "string", "Key must be a string type.")
ClientProcess.removeCallback(self.id, key) ClientProcess.removeCallback(self.id, key)
table.remove(self.fn, table.find(self.fn, key))
self.IsConnected = #self.fn > 0
end end
function Client:Destroy() function Client:Destroy()
self:DisconnectAll() self:DisconnectAll()
self._buffer:remove()
ClientProcess.remove(self.id)
Serdes.decrement()
table.clear(self)
setmetatable(self, nil) setmetatable(self, nil)
end end

View file

@ -1,4 +1,5 @@
--!strict --!strict
--!optimize 2
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local Type = require(script.Parent.Type) local Type = require(script.Parent.Type)

View file

@ -1,4 +1,6 @@
--!strict --!strict
--!native
--!optimize 2
local Server = {} local Server = {}
Server.__index = Server Server.__index = Server
@ -10,12 +12,21 @@ local ServerProcess = require(script.Parent.ServerProcess)
local Assert = require(Util.Assert) local Assert = require(Util.Assert)
local Key = require(Util.Key) local Key = require(Util.Key)
local Serdes = require(Util.Serdes) local Serdes = require(Util.Serdes)
local Buffer = require(Util.Buffer)
function Server.new(Identifier: string, rateLimit: Type.rateLimitArg?) function Server.new(Identifier: string, conf: Type.ServerConf?)
local self = setmetatable({}, Server) local self = setmetatable({}, Server)
self.id = Serdes(Identifier)
self._buffer = Buffer.new()
self._buffer:wu8(Serdes.increment(Identifier))
self.id = Buffer.convert(self._buffer:build())
self.fn = {} self.fn = {}
ServerProcess.add(self.id, Identifier, rateLimit or { maxEntrance = 200, interval = 2 }) self._conf = table.freeze(conf or {})
self.IsConnected = false
ServerProcess.add(self.id, Identifier, conf or { rateLimit = { maxEntrance = 200, interval = 2 } })
self._buffer:remove()
return self return self
end end
@ -29,6 +40,20 @@ function Server:Fires(reliable: boolean, ...: any)
end end
end end
function Server:FireExcept(reliable: boolean, except: { Player }, ...: any)
for _, player: Player in ipairs(Players:GetPlayers()) do
if table.find(except, player) then continue end
ServerProcess.insertQueue(self.id, reliable, player, ...)
end
end
function Server:FireIn(reliable: boolean, range: number, from: Vector3, data: { any }, except: { Player }?)
for _, player: Player in ipairs(Players:GetPlayers()) do
if (except and table.find(except, player)) or not player.Character or not player.Character.PrimaryPart or (player.Character.PrimaryPart.Position - from).Magnitude < range then continue end
ServerProcess.insertQueue(self.id, reliable, player, table.unpack(data))
end
end
function Server:Invoke(timeout: number, player: Player, ...: any): any function Server:Invoke(timeout: number, player: Player, ...: any): any
return ServerProcess.insertRequest(self.id, timeout, player, ...) return ServerProcess.insertRequest(self.id, timeout, player, ...)
end end
@ -37,15 +62,17 @@ function Server:Connect(callback: (plyer: Player, args: any) -> ()): string
local key = tostring(Key()) local key = tostring(Key())
table.insert(self.fn, key) table.insert(self.fn, key)
ServerProcess.addCallback(self.id, key, callback) ServerProcess.addCallback(self.id, key, callback)
self.IsConnected = #self.fn > 0
return key return key
end end
function Server:Once(callback: (plyer: Player, args: any) -> ()): string function Server:Once(callback: (plyer: Player, args: any) -> ()): string
local key = tostring(Key()) local key = tostring(Key())
table.insert(self.fn, key) table.insert(self.fn, key)
ServerProcess.addCallback(self.id, key, function(...) self.IsConnected = #self.fn > 0
ServerProcess.addCallback(self.id, key, function(player: Player, ...: any?)
self:Disconnect(key) self:Disconnect(key)
task.spawn(callback, ...) task.spawn(callback, player, ...)
end) end)
return key return key
end end
@ -59,19 +86,25 @@ function Server:Wait()
end end
function Server:DisconnectAll() function Server:DisconnectAll()
for idx, key: string in self.fn do for _, key: string in self.fn do
ServerProcess.removeCallback(self.id, key) self:Disconnect(key)
table.remove(self.fn, idx)
end end
end end
function Server:Disconnect(key: string) function Server:Disconnect(key: string): boolean
Assert(typeof(key) == "string", "Key must be a string type.") Assert(typeof(key) == "string", "Key must be a string type.")
ServerProcess.removeCallback(self.id, key) ServerProcess.removeCallback(self.id, key)
table.remove(self.fn, table.find(self.fn, key))
self.IsConnected = #self.fn > 0
return table.find(self.fn, key) == nil
end end
function Server:Destroy() function Server:Destroy()
self:DisconnectAll() self:DisconnectAll()
self._buffer:remove()
ServerProcess.remove(self.id)
Serdes.decrement()
table.clear(self)
setmetatable(self, nil) setmetatable(self, nil)
end end

View file

@ -1,260 +0,0 @@
--!native
--!strict
local ServerProcess = {}
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Util = script.Parent.Parent.Util
local Type = require(script.Parent.Parent.Type)
local Event = require(script.Parent.Parent.Event)
local Spawn = require(Util.Spawn)
local Key = require(Util.Key)
local RateLimit = require(Util.RateLimit)
local serverQueue: Type.QueueMap = {}
local unreliableServerQueue: Type.QueueMap = {}
local serverCallback: Type.CallbackMap = {}
local serverRequestQueue: Type.QueueMap = {}
local queueOut: {
[Player]: {
[string]: {any},
}
} = {}
local queueIn: {
[string]: {
[Player]: {any},
}
} = {}
local queueInRequest: {
[number]: {
[string]: {
[Player]: {any}
}
}
} = {}
local queueOutRequest: {
[number]: {
[string]: {
[Player]: {any}
}
}
} = {}
queueInRequest[1] = {}
queueInRequest[2] = {}
queueOutRequest[1] = {}
queueOutRequest[2] = {}
local ReliableEvent = Event.Reliable
local UnreliableEvent = Event.Unreliable
local RequestEvent = Event.Request
local function initializeEachPlayer(player: Player)
if not player then return end
if not queueOut[player] then
queueOut[player] = {}
end
for Identifier: string,_ in serverQueue do
if not queueOut[player][Identifier] then
queueOut[player][Identifier] = {}
end
if not serverRequestQueue[Identifier][player] then
serverRequestQueue[Identifier][player] = {}
end
if not queueIn[Identifier][player] then
queueIn[Identifier][player] = {}
end
if not queueInRequest[1][Identifier][player] then
queueInRequest[1][Identifier][player] = {}
queueInRequest[2][Identifier][player] = {}
end
if not queueOutRequest[1][Identifier][player] then
queueOutRequest[1][Identifier][player] = {}
queueOutRequest[2][Identifier][player] = {}
end
end
end
Players.PlayerAdded:Connect(initializeEachPlayer)
function ServerProcess.insertQueue(Identifier: string, reliable: boolean, player: Player, ...: any)
if not reliable then
if not unreliableServerQueue[Identifier][player] then
unreliableServerQueue[Identifier][player] = {}
end
table.insert(unreliableServerQueue[Identifier][player], { ... })
return
end
if not serverQueue[Identifier][player] then
serverQueue[Identifier][player] = {}
end
table.insert(serverQueue[Identifier][player], { ... })
end
function ServerProcess.insertRequest(Identifier: string, timeout: number, player: Player, ...: any)
if not serverQueue[Identifier][player] then
serverQueue[Identifier][player] = {}
end
local yieldThread: thread, start = coroutine.running(), os.clock()
local cancel = task.delay(timeout, function()
task.spawn(yieldThread, nil)
end)
table.insert(serverRequestQueue[Identifier][player], { tostring(Key()), function(...: any)
if (os.clock() - start) > timeout then return end
task.cancel(cancel)
task.spawn(yieldThread, ...)
end :: any, { ... } :: any })
return coroutine.yield()
end
function ServerProcess.add(Identifier: string, originId: string, ratelimit: Type.rateLimitArg)
if not serverQueue[Identifier] then
RateLimit.create(originId, ratelimit.maxEntrance or 200, ratelimit.interval or 2)
serverQueue[Identifier] = {}
unreliableServerQueue[Identifier] = {}
serverCallback[Identifier] = {}
serverRequestQueue[Identifier] = {}
queueIn[Identifier] = {}
queueInRequest[1][Identifier] = {}
queueInRequest[2][Identifier] = {}
queueOutRequest[1][Identifier] = {}
queueOutRequest[2][Identifier] = {}
for _, player: Player in ipairs(Players:GetPlayers()) do
task.spawn(initializeEachPlayer, player)
end
end
end
function ServerProcess.addCallback(Identifier: string, key: string, callback)
serverCallback[Identifier][key] = callback
end
function ServerProcess.removeCallback(Identifier: string, key: string)
serverCallback[Identifier][key] = nil
end
function ServerProcess.start()
RunService.PostSimulation:Connect(function()
for Identifier: string, players in unreliableServerQueue do
for player: Player, data: any in players do
if #data == 0 then continue end
UnreliableEvent:FireClient(player, Identifier, data)
table.clear(data)
end
end
for _, player: Player in ipairs(Players:GetPlayers()) do
if not queueOut[player] then continue end
for Identifier: string, data: any in queueOut[player] do
if #data == 0 then continue end
ReliableEvent:FireClient(player, Identifier, data)
table.clear(data)
end
end
for Identifier: string, players in serverQueue do
for player: Player, data in players do
if #data > 0 and queueOut[player] then
queueOut[player][Identifier] = table.clone(data)
table.clear(data)
end
if #serverRequestQueue[Identifier][player] > 0 then
for _, requestData in serverRequestQueue[Identifier][player] do
if not requestData[3] then continue end
table.insert(queueOutRequest[1][Identifier][player], { requestData[1], requestData[3] })
table.remove(requestData, #requestData)
end
end
if serverCallback[Identifier] then
if #queueIn[Identifier][player] > 0 then
for _, packedDatas: any in queueIn[Identifier][player] do
if #packedDatas == 0 then continue end
for _, v: any in packedDatas do
for _, fn: any in serverCallback[Identifier] do
Spawn(fn, player, table.unpack(v))
end
end
end
table.clear(queueIn[Identifier][player])
end
if #queueInRequest[1][Identifier][player] > 0 then
for idx, packetDatas: any in queueInRequest[1][Identifier][player] do
if #packetDatas == 0 then continue end
for _, packetData in packetDatas do
for _, fn: any in serverCallback[Identifier] do
Spawn(function()
local requestReturn = { fn(player, table.unpack(packetData[2])) }
table.insert(queueOutRequest[2][Identifier][player], { packetData[1], requestReturn })
end)
end
end
end
table.clear(queueInRequest[1][Identifier][player])
end
if #queueInRequest[2][Identifier][player] > 0 then
for _, packetDatas: any in queueInRequest[2][Identifier][player] do
for _, packetData in packetDatas do
if #packetData == 1 then continue end
for idx, serverRequest in serverRequestQueue[Identifier][player] do
if serverRequest[1] == packetData[1] then
Spawn(serverRequest[2], table.unpack(packetData[2]))
table.remove(serverRequestQueue[Identifier], idx)
break
end
end
end
end
table.clear(queueInRequest[2][Identifier][player])
end
for player: Player, requestsData: any in queueOutRequest[1][Identifier] do
if #requestsData == 0 then continue end
RequestEvent:FireClient(player, Identifier, "\1", requestsData)
table.clear(requestsData)
end
for player: Player, toReturnDatas: any in queueOutRequest[2][Identifier] do
if #toReturnDatas == 0 then continue end
RequestEvent:FireClient(player, Identifier, "\0", toReturnDatas)
table.clear(toReturnDatas)
end
end
end
end
end)
local function onServerNetworkReceive(player: Player, Identifier: string, data: any)
if not Identifier or not data then return end
if not serverQueue[Identifier] then
serverQueue[Identifier] = {}
end
if not serverQueue[Identifier][player] then
serverQueue[Identifier][player] = {}
end
if not queueIn[Identifier][player] then
queueIn[Identifier][player] = {}
end
table.insert(queueIn[Identifier][player], data)
end
ReliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
UnreliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
RequestEvent.OnServerEvent:Connect(function(player: Player, Identifier: string, action: string, data: any)
if not Identifier or not data then return end
if not queueInRequest[1][Identifier][player] then
queueInRequest[1][Identifier][player] = {}
queueInRequest[2][Identifier][player] = {}
end
if not serverQueue[Identifier][player] then
serverQueue[Identifier][player] = {}
end
if action == "\1" then
table.insert(queueInRequest[1][Identifier][player], data)
else
table.insert(queueInRequest[2][Identifier][player], data)
end
end)
end
for _, player: Player in ipairs(Players:GetPlayers()) do
task.spawn(initializeEachPlayer, player)
end
return ServerProcess

View file

@ -0,0 +1,371 @@
--!native
--!strict
--!optimize 2
local ServerProcess = {}
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local Util = script.Parent.Parent.Util
local Type = require(script.Parent.Parent.Type)
local Event = require(script.Parent.Parent.Event)
local Spawn = require(Util.Spawn)
local Key = require(Util.Key)
local RateLimit = require(Util.RateLimit)
local Buffer = require(Util.Buffer)
local serverQueue: Type.QueueMap = {}
local unreliableServerQueue: Type.QueueMap = {}
local serverCallback: Type.CallbackMap = {}
local serverRequestQueue: Type.QueueMap = {}
local registeredIdentifier: { [string]: boolean } = {}
local queueOut: {
[Player]: {
[string]: {any},
}
} = {}
local queueInRequest: {
[number]: {
[string]: {
[Player]: {any}
}
}
} = {}
local queueOutRequest: {
[number]: {
[string]: {
[Player]: {any}
}
}
} = {}
queueInRequest[1] = {}
queueInRequest[2] = {}
queueOutRequest[1] = {}
queueOutRequest[2] = {}
local ReliableEvent = Event.Reliable
local UnreliableEvent = Event.Unreliable
local RequestEvent = Event.Request
RateLimit.Protect()
local function initializeEachPlayer(player: Player)
if not player then return end
if not queueOut[player] then
queueOut[player] = {}
end
for Identifier: string in registeredIdentifier do
if not player then break end
if not queueOut[player][Identifier] then
queueOut[player][Identifier] = {}
end
if not serverRequestQueue[Identifier] then
serverRequestQueue[Identifier] = {}
end
if not serverRequestQueue[Identifier][player] then
serverRequestQueue[Identifier][player] = {}
end
if not queueOutRequest[1][Identifier] then
queueOutRequest[1][Identifier] = {}
end
if not queueOutRequest[2][Identifier] then
queueOutRequest[2][Identifier] = {}
end
if not queueInRequest[1][Identifier][player] then
queueInRequest[1][Identifier][player] = {}
queueInRequest[2][Identifier][player] = {}
end
if not queueOutRequest[1][Identifier][player] then
queueOutRequest[1][Identifier][player] = {}
queueOutRequest[2][Identifier][player] = {}
end
end
end
Players.PlayerAdded:Connect(initializeEachPlayer)
Players.PlayerRemoving:Connect(function(player: Player)
if not player then return end
if queueOut[player] then
queueOut[player] = nil
end
for _, map in { serverQueue, unreliableServerQueue, serverRequestQueue } do
for Identifier: string in map do
map[Identifier][player] = nil
end
end
for i=1,2 do
for Identifier: string in queueInRequest[i] do
if queueInRequest[i][Identifier][player] then
queueInRequest[i][Identifier][player] = nil
end
end
for Identifier: string in queueOutRequest[i] do
if queueOutRequest[i][Identifier][player] then
queueOutRequest[i][Identifier][player] = nil
end
end
end
end)
function ServerProcess.insertQueue(Identifier: string, reliable: boolean, player: Player, ...: any)
if not reliable then
if not unreliableServerQueue[Identifier] then
unreliableServerQueue[Identifier] = {}
end
if not unreliableServerQueue[Identifier][player] then
unreliableServerQueue[Identifier][player] = {}
end
table.insert(unreliableServerQueue[Identifier][player], { ... })
return
end
if not serverQueue[Identifier] then
serverQueue[Identifier] = {}
end
if not serverQueue[Identifier][player] then
serverQueue[Identifier][player] = {}
end
table.insert(serverQueue[Identifier][player], { ... })
end
function ServerProcess.insertRequest(Identifier: string, timeout: number, player: Player, ...: any)
if not serverRequestQueue[Identifier] then
serverRequestQueue[Identifier] = {}
end
if not serverRequestQueue[Identifier][player] then
serverRequestQueue[Identifier][player] = {}
end
local yieldThread: thread, start = coroutine.running(), os.clock()
local cancel = task.delay(timeout, function()
task.spawn(yieldThread, nil)
end)
table.insert(serverRequestQueue[Identifier][player], { tostring(Key()), function(...: any)
if (os.clock() - start) > timeout then return end
task.cancel(cancel)
task.spawn(yieldThread, ...)
end :: any, { ... } :: any })
return coroutine.yield()
end
function ServerProcess.add(Identifier: string, originId: string, conf: Type.ServerConf)
if not registeredIdentifier[Identifier] then
registeredIdentifier[Identifier] = true
RateLimit.create(originId, conf.rateLimit and conf.rateLimit.maxEntrance or 200, conf.rateLimit and conf.rateLimit.interval or 2)
if not serverQueue[Identifier] then
serverQueue[Identifier] = {}
end
if not serverRequestQueue[Identifier] then
serverRequestQueue[Identifier] = {}
end
if not serverCallback[Identifier] then
serverCallback[Identifier] = {}
end
if not unreliableServerQueue[Identifier] then
unreliableServerQueue[Identifier] = {}
end
if not queueInRequest[1][Identifier] then
queueInRequest[1][Identifier] = {}
end
if not queueInRequest[2][Identifier] then
queueInRequest[2][Identifier] = {}
end
if not queueOutRequest[1][Identifier] then
queueOutRequest[1][Identifier] = {}
end
if not queueOutRequest[2][Identifier] then
queueOutRequest[2][Identifier] = {}
end
for _, player: Player in ipairs(Players:GetPlayers()) do
task.spawn(initializeEachPlayer, player)
end
end
end
function ServerProcess.remove(Identifier: string)
if not registeredIdentifier[Identifier] then return end
registeredIdentifier[Identifier] = nil
serverQueue[Identifier] = nil
serverRequestQueue[Identifier] = nil
serverCallback[Identifier] = nil
unreliableServerQueue[Identifier] = nil
queueInRequest[1][Identifier] = nil
queueInRequest[2][Identifier] = nil
queueOutRequest[1][Identifier] = nil
queueOutRequest[2][Identifier] = nil
end
function ServerProcess.addCallback(Identifier: string, key: string, callback)
serverCallback[Identifier][key] = callback
end
function ServerProcess.removeCallback(Identifier: string, key: string)
serverCallback[Identifier][key] = nil
end
function ServerProcess.start()
debug.setmemorycategory("Warp")
RunService.PostSimulation:Connect(function()
-- Unreliable
for Identifier: string, players in unreliableServerQueue do
for player: Player, content: any in players do
if #content == 0 then continue end
for _, unpacked in content do
UnreliableEvent:FireClient(player, Buffer.revert(Identifier), Buffer.write(unpacked))
end
unreliableServerQueue[Identifier][player] = nil
end
unreliableServerQueue[Identifier] = nil
end
-- Reliable
for Identifier: string, contents: { [Player]: { any } } in serverQueue do
for player, content: any in contents do
if #content > 0 and queueOut[player] then
for _, unpacked in content do
ReliableEvent:FireClient(player, Buffer.revert(Identifier), Buffer.write(unpacked))
end
end
serverQueue[Identifier][player] = nil
end
serverQueue[Identifier] = nil
end
-- Sent new invokes
for Identifier: string, contents in queueOutRequest[1] do
for player: Player, requestsData: any in contents do
if #requestsData > 0 then
RequestEvent:FireClient(player, Buffer.revert(Identifier), "\1", requestsData)
end
queueOutRequest[1][Identifier][player] = nil
end
queueOutRequest[1][Identifier] = nil
end
-- Sent returning invokes
for Identifier: string, contents in queueOutRequest[2] do
for player: Player, toReturnDatas: any in contents do
if #toReturnDatas > 0 then
RequestEvent:FireClient(player, Buffer.revert(Identifier), "\0", toReturnDatas)
end
queueOutRequest[2][Identifier][player] = nil
end
queueOutRequest[2][Identifier] = nil
end
for Identifier: string in registeredIdentifier do
if serverRequestQueue[Identifier] then
for player, content in serverRequestQueue[Identifier] do
if #content == 0 then serverRequestQueue[Identifier][player] = nil continue end
for _, requestData in content do
if not requestData[3] then continue end
if not queueOutRequest[1][Identifier] then
queueOutRequest[1][Identifier] = {}
end
if not queueOutRequest[1][Identifier][player] then
queueOutRequest[1][Identifier][player] = {}
end
table.insert(queueOutRequest[1][Identifier][player], { requestData[1], requestData[3] })
requestData[3] = nil
end
end
end
local callback = serverCallback[Identifier] or nil
if not callback then continue end
-- Return Invoke
for player, content in queueInRequest[1][Identifier] do
if not callback then break end
for _, packetDatas in content do
if not callback then break end
if #packetDatas == 0 then continue end
for _, fn: any in callback do
for i=1,#packetDatas do
if not packetDatas[i] then continue end
local packetData1 = packetDatas[i][1]
local packetData2 = packetDatas[i][2]
Spawn(function()
local requestReturn = { fn(player, table.unpack(packetData2)) }
if not queueOutRequest[2][Identifier] then
queueOutRequest[2][Identifier] = {}
end
if not queueOutRequest[2][Identifier][player] then
queueOutRequest[2][Identifier][player] = {}
end
table.insert(queueOutRequest[2][Identifier][player], { packetData1, requestReturn })
packetData1 = nil
packetData2 = nil
end)
end
end
end
queueInRequest[1][Identifier][player] = nil
end
-- Call to Invoke
for player, content in queueInRequest[2][Identifier] do
if not callback then break end
for _, packetDatas in content do
for _, packetData in packetDatas do
if not callback then break end
if #packetData == 1 then continue end
local data = serverRequestQueue[Identifier][player]
for i=1,#data do
local serverRequest = data[i]
if not serverRequest then continue end
if serverRequest[1] == packetData[1] then
Spawn(serverRequest[2], table.unpack(packetData[2]))
table.remove(data, i)
break
end
end
end
end
queueInRequest[2][Identifier][player] = nil
end
end
end)
local function onServerNetworkReceive(player: Player, Identifier: buffer | string, data: buffer, ref: { any }?)
if not Identifier or typeof(Identifier) ~= "buffer" or not data or typeof(data) ~= "buffer" then return end
Identifier = Buffer.convert(Identifier :: buffer)
if not registeredIdentifier[Identifier :: string] then return end
local read = Buffer.read(data, ref)
if not read then return end
local callback = serverCallback[Identifier :: string]
if not callback then return end
for _, fn: any in callback do
Spawn(fn, player, table.unpack(read))
end
end
ReliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
UnreliableEvent.OnServerEvent:Connect(onServerNetworkReceive)
RequestEvent.OnServerEvent:Connect(function(player: Player, Identifier: any, action: string, data: any)
if not Identifier or not data then return end
Identifier = Buffer.convert(Identifier)
if not queueInRequest[1][Identifier][player] then
queueInRequest[1][Identifier][player] = {}
end
if not queueInRequest[2][Identifier][player] then
queueInRequest[2][Identifier][player] = {}
end
if not serverQueue[Identifier] then
serverQueue[Identifier] = {}
end
if not serverQueue[Identifier][player] then
serverQueue[Identifier][player] = {}
end
if action == "\1" then
table.insert(queueInRequest[1][Identifier][player], data)
else
table.insert(queueInRequest[2][Identifier][player], data)
end
end)
end
for _, player: Player in ipairs(Players:GetPlayers()) do
task.spawn(initializeEachPlayer, player)
end
return ServerProcess

View file

@ -1,83 +0,0 @@
--!strict
--!native
local Signal = {}
Signal.__index = Signal
local Util = script.Parent.Util
local Key = require(Util.Key)
local Spawn = require(Util.Spawn)
local Assert = require(Util.Assert)
local Signals = {}
function Signal.new(Identifier: string)
Assert(typeof(Identifier) == "string", `[Signal]: Identifier must be a string type, got {typeof(Identifier)}`)
if not Signals[Identifier] then
local signal = setmetatable({}, Signal)
signal.id = Identifier
signal.fn = {}
Signals[Identifier] = signal
return signal
end
return Signals[Identifier]
end
function Signal:Fire(...: any): ()
for _, fn in self.fn do
Spawn(fn, ...)
end
end
function Signal:FireTo(Identifier: string, ...: any): ()
if not Identifier or not Signals[Identifier] then return end
Signal.Fire(Signals[Identifier], ...)
end
function Signal:Invoke(key: string, ...: any): (...any?)
if not key or not self.fn[key] then return nil end
return self.fn[key](...)
end
function Signal:InvokeTo(Identifier: string, key: string, ...: any): (...any?)
if not Identifier or not Signals[Identifier] or not key then return nil end
return Signal.Invoke(Signals[Identifier], key, ...)
end
function Signal:Connect(fn: (...any) -> ()): string
local key = tostring(Key())
self.fn[key] = fn
return key
end
function Signal:Once(fn: (...any) -> ()): string
local key: string
key = self:Connect(function(...)
self:Disconnect(key)
Spawn(fn, ...)
end)
return key
end
function Signal:Wait(): number
local thread, t = coroutine.running(), os.clock()
self:Once(function()
task.spawn(thread, os.clock()-t)
end)
return coroutine.yield()
end
function Signal:Disconnect(key: string): ()
if not key then return end
self.fn[key] = nil
end
function Signal:DisconnectAll(): ()
table.clear(self.fn)
end
function Signal:Destroy(): ()
self:DisconnectAll()
setmetatable(self, nil)
end
return Signal.new :: typeof(Signal.new)

View file

@ -1,5 +1,6 @@
--!strict --!strict
--!native --!native
--!optimize 2
local Dedicated = {} local Dedicated = {}
Dedicated.__index = Dedicated Dedicated.__index = Dedicated
@ -11,6 +12,7 @@ function Dedicated.new(signal: any, handler: (...any) -> ())
end end
function Dedicated:Disconnect() function Dedicated:Disconnect()
table.clear(self)
setmetatable(self, nil) setmetatable(self, nil)
end end

View file

@ -1,5 +1,6 @@
--!strict --!strict
--!native --!native
--!optimize 2
local Signal = {} local Signal = {}
Signal.__index = Signal Signal.__index = Signal
@ -21,15 +22,15 @@ function Signal.new(Identifier: string)
return Signals[Identifier] return Signals[Identifier]
end end
function Signal:Connect(fn: (...any) -> ()): string function Signal:Connect(fn: (...any) -> (), optKey: string?): string
local key = tostring(Key()) local key: typeof(Signal) = optKey or tostring(Key()) :: any
self[key] = DedicatedSignal(self, fn) self[key] = DedicatedSignal(self, fn)
return key return key :: any
end end
function Signal:Once(fn: (...any) -> ()): string function Signal:Once(fn: (...any) -> ()): string
local key: string local key: string
key = self:Connect(function(...) key = self:Connect(function(...: any)
self:Disconnect(key) self:Disconnect(key)
task.spawn(fn, ...) task.spawn(fn, ...)
end) end)
@ -37,7 +38,9 @@ function Signal:Once(fn: (...any) -> ()): string
end end
function Signal:Disconnect(key: string) function Signal:Disconnect(key: string)
if not self[key] then return end
self[key]:Disconnect() self[key]:Disconnect()
self[key] = nil
end end
function Signal:DisconnectAll(): () function Signal:DisconnectAll(): ()
@ -45,16 +48,22 @@ function Signal:DisconnectAll(): ()
end end
function Signal:Wait(): number function Signal:Wait(): number
local thread, t = coroutine.running(), os.clock() local t, thread = os.clock(), coroutine.running()
self:Once(function() self:Once(function()
task.spawn(thread, os.clock()-t) task.spawn(thread, os.clock()-t)
end) end)
return coroutine.yield() return coroutine.yield()
end end
function Signal:DeferFire(...: any): ()
for _, handle in self do
task.defer(handle.fn, ...)
end
end
function Signal:Fire(...: any): () function Signal:Fire(...: any): ()
for _, handle in self do for _, handle in self do
pcall(handle.fn, ...) task.spawn(handle.fn, ...)
end end
end end
@ -72,7 +81,7 @@ end
function Signal:InvokeTo(signal: string, key: string, ...: any): () function Signal:InvokeTo(signal: string, key: string, ...: any): ()
if not Signals[signal] then return end if not Signals[signal] then return end
return Signal.Invoke(Signals[signal], ...) return Signal.Invoke(Signals[signal], key, ...)
end end
function Signal:Destroy(): () function Signal:Destroy(): ()

View file

@ -1,30 +1,44 @@
--!strict --!strict
export type rateLimitArg = { type rateLimitArg = {
maxEntrance: number?, maxEntrance: number?,
interval: number?, interval: number?,
} }
export type ServerConf = {
rateLimit: rateLimitArg?,
}
export type ClientConf = {
yieldWait: number?,
}
export type Middleware = {
middleware: (self: Middleware, middleware: (...any) -> (...any)) -> (),
key: (self: Middleware) -> string,
destroy: (self: Middleware) -> (),
}
export type Client = { export type Client = {
Fire: (self: Client, reliable: boolean, ...any) -> (), Fire: (self: Client, reliable: boolean, ...any) -> (),
Invoke: (self: Client, timeout: number, ...any) -> any, Invoke: (self: Client, timeout: number, ...any) -> any,
Connect: (self: Client, callback: (...any) -> ()) -> string, Connect: (self: Client, callback: (...any) -> ()) -> Middleware,
Once: (self: Client, callback: (player: Player, ...any) -> ()) -> string, Once: (self: Client, callback: (player: Player, ...any) -> ()) -> Middleware,
Disconnect: (self: Client, key: string) -> (), Disconnect: (self: Client, key: string) -> (),
DisconnectAll: (self: Client) -> (), DisconnectAll: (self: Client) -> (),
Destroy: (self: Client) -> (),
Wait: (self: Client) -> number, Wait: (self: Client) -> number,
Destroy: (self: Client) -> (),
} }
export type Server = { export type Server = {
Fire: (self: Server, reliable: boolean, player: Player, ...any) -> (), Fire: (self: Server, reliable: boolean, player: Player, ...any) -> (),
Fires: (self: Server, reliable: boolean, ...any) -> (), Fires: (self: Server, reliable: boolean, ...any) -> (),
Invoke: (self: Server, timeout: number, player: Player, ...any) -> any, Invoke: (self: Server, timeout: number, player: Player, ...any) -> any,
Connect: (self: Server, callback: (player: Player, ...any) -> ()) -> string, Connect: (self: Server, callback: (player: Player, ...any) -> ()) -> Middleware,
Once: (self: Server, callback: (player: Player, ...any) -> ()) -> string, Once: (self: Server, callback: (player: Player, ...any) -> ()) -> Middleware,
Disconnect: (self: Server, key: string) -> (), Disconnect: (self: Server, key: string) -> (),
DisconnectAll: (self: Server) -> (), DisconnectAll: (self: Server) -> (),
Destroy: (self: Server) -> (),
Wait: (self: Server) -> number, Wait: (self: Server) -> number,
Destroy: (self: Server) -> (),
} }
export type Signal = { export type Signal = {

View file

@ -1,4 +1,6 @@
--!strict --!strict
return function(condition: (any), errorMessage: string?): () --!native
if not (condition) then error(errorMessage, 2) end --!optimize 2
return function(condition: (any), errorMessage: string, level: number?): ()
if not (condition) then error(`Warp: {errorMessage}`, level or 2) end
end end

View file

@ -0,0 +1,265 @@
--!strict
--!native
--!optimize 2
local DedicatedBuffer = {}
DedicatedBuffer.__index = DedicatedBuffer
local create = buffer.create
local copy = buffer.copy
local writei8 = buffer.writei8
local writei16 = buffer.writei16
local writei32 = buffer.writei32
local writeu8 = buffer.writeu8
local writeu16 = buffer.writeu16
local writeu32 = buffer.writeu32
local writef32 = buffer.writef32
local writef64 = buffer.writef64
local writestring = buffer.writestring
local default: { [string]: number } = {
point = 0,
next = 0,
size = 128,
bufferSize = 128,
}
function DedicatedBuffer.copy(self: any, offset: number, b: buffer?, src: buffer?, srcOffset: number?, count: number?)
if not b then
copy(create(count or default.size), offset, src or self.buffer, srcOffset, count)
else
copy(b, offset, src or self.buffer, srcOffset, count)
end
end
function DedicatedBuffer.alloc(self: any, byte: number)
local size: number = self.size
local b: buffer = self.buffer
while self.next + byte >= size do
size = math.floor(size * 1.25) -- +25% increase
end
local newBuffer: buffer = create(size)
copy(newBuffer, 0, b)
b = newBuffer
self.point = self.next
self.buffer = b
self.next += byte
end
function DedicatedBuffer.build(self: any): buffer
local p: number = self.next > self.point and self.next or self.point
local build: buffer = create(p)
copy(build, 0, self.buffer, 0, p)
return build
end
function DedicatedBuffer.buildAndRemove(self: any): (buffer, (any)?)
local p: number = self.next > self.point and self.next or self.point
local build: buffer = create(p)
local ref = #self.ref > 0 and table.clone(self.ref) or nil
copy(build, 0, self.buffer, 0, p)
self:remove()
return build, ref
end
function DedicatedBuffer.wi8(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 1)
writei8(self.buffer, self.point, val)
end
function DedicatedBuffer.wi16(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 2)
writei16(self.buffer, self.point, val)
end
function DedicatedBuffer.wi32(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 4)
writei32(self.buffer, self.point, val)
end
function DedicatedBuffer.wu8(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 1)
writeu8(self.buffer, self.point, val)
end
function DedicatedBuffer.wu16(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 2)
writeu16(self.buffer, self.point, val)
end
function DedicatedBuffer.wu32(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 4)
writeu32(self.buffer, self.point, val)
end
function DedicatedBuffer.wf32(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 4)
writef32(self.buffer, self.point, val)
end
function DedicatedBuffer.wf64(self: any, val: number, alloc: number?)
if not val then return end
self:alloc(alloc or 8)
writef64(self.buffer, self.point, val)
end
function DedicatedBuffer.wstring(self: any, val: string)
if not val then return end
self:alloc(#val)
writestring(self.buffer, self.point, val)
end
function DedicatedBuffer.wType(self: any, ref: number)
writeu8(self.buffer, self.point, ref)
self.point += 1
end
function DedicatedBuffer.wRef(self: any, value: any, alloc: number?)
if not value then return end
self:alloc(alloc or 1)
table.insert(self.ref, value)
local index = #self.ref
writeu8(self.buffer, self.point, index)
self.point += 1
end
function DedicatedBuffer.pack(self: any, data: {any})
if typeof(data) == "nil" then
self:wi8(0)
elseif typeof(data) == "Instance" then
self:wi8(-1) -- Instance marker
self:wRef(data)
elseif typeof(data) == "table" then
--local isArray = (next(data) ~= nil and #data > 0) and true or false
local isArray = true
local count = 0
for k in data do
count += 1
if typeof(k) ~= "number" or math.floor(k) ~= k then
isArray = false
end
end
if isArray then
self:wi8(-2) -- array marker
self:wu16(count) -- use 32-bit length
for _, v in data do
self:pack(v)
end
else
self:wi8(-3) -- dictionary marker
self:wu16(count) -- number of key-value pairs
for k, v in data do
self:pack(k) -- pack the key
self:pack(v) -- pack the value
end
end
elseif typeof(data) == "EnumItem" then
self:wi8(-4)
self:wi8(#`{data.EnumType}`)
self:wstring(`{data.EnumType}`)
self:wu8(data.Value)
elseif typeof(data) == "BrickColor" then
self:wi8(-5)
self:wi16(data.Number)
elseif typeof(data) == "Enum" then
self:wi8(-6)
self:wi8(#`{data}`)
self:wstring(`{data}`)
elseif typeof(data) == "number" then
if math.floor(data) == data then -- Integer
if data >= 0 and data <= 255 then
self:wi8(1) -- u8 marker
self:wu8(data)
elseif data >= -32768 and data <= 32767 then
self:wi8(2) -- i16 marker
self:wi16(data)
elseif data >= -2147483647 and data <= 2147483647 then
self:wi8(3) -- i32 marker
self:wi32(data)
else
self:wi8(4) -- f64 marker
self:wf64(data)
end
else
self:wi8(4) -- f64 marker
self:wf64(data)
end
elseif typeof(data) == "boolean" then
self:wi8(5) -- boolean marker
self:wu8(data and 1 or 0)
elseif typeof(data) == "string" then
local length = #data
if length <= 255 then
self:wi8(6)
self:wu8(length)
elseif length <= 65535 then
self:wi8(7)
self:wu16(length)
else
self:wi8(8)
self:wi32(length)
end
self:wstring(data)
elseif typeof(data) == "Vector3" then
self:wi8(9) -- Vector3 marker
self:wf32(data.X)
self:wf32(data.Y)
self:wf32(data.Z)
elseif typeof(data) == "Vector2" then
self:wi8(10) -- Vector2 marker
self:wf32(data.X)
self:wf32(data.Y)
elseif typeof(data) == "CFrame" then
self:wi8(11) -- CFrame marker
for _, v in {data:GetComponents()} do
self:wf32(v)
end
elseif typeof(data) == "Color3" then
self:wi8(12) -- Color3 marker
self:wu8(data.R * 255)
self:wu8(data.G * 255)
self:wu8(data.B * 255)
else
warn(`Unsupported data type: {typeof(data)} value: {data}`)
end
end
function DedicatedBuffer.flush(self: any)
self.point = default.point
self.next = default.next
self.size = default.size
self.buffer = create(default.bufferSize)
table.clear(self.ref)
end
function DedicatedBuffer.new()
return setmetatable({
point = default.point,
next = default.next,
size = default.size,
buffer = create(default.bufferSize),
ref = {},
}, DedicatedBuffer)
end
function DedicatedBuffer.remove(self: any)
self:flush()
table.clear(self)
setmetatable(self, nil)
end
export type DedicatedType = typeof(DedicatedBuffer.new())
return DedicatedBuffer.new :: typeof(DedicatedBuffer.new)

View file

@ -0,0 +1,149 @@
--!strict
--!native
--!optimize 2
local Buffer = {}
Buffer.__index = Buffer
local Dedicated = require(script.Dedicated)
local tostring = buffer.tostring
local fromstring = buffer.fromstring
local readu8 = buffer.readu8
local readi8 = buffer.readi8
local readu16 = buffer.readu16
local readi16 = buffer.readi16
local readi32 = buffer.readi32
local readf32 = buffer.readf32
local readf64 = buffer.readf64
local readstring = buffer.readstring
local len = buffer.len
local function readValue(b: buffer, position: number, ref: { any }?): (any, number)
local typeByte = readi8(b, position)
position += 1
if typeByte == 0 then -- nil
return nil, position
elseif typeByte == -1 then -- Instance
if not ref or #ref == 0 then
return nil, position + 1
end
local value = ref[readu8(b, position)]
if typeof(value) == "Instance" then
return value, position + 1
end
return nil, position + 1
elseif typeByte == -2 then -- array
local length = readu16(b, position)
position += 2
local array = {}
for _ = 1, length do
local value
value, position = readValue(b, position, ref)
table.insert(array, value)
end
return array, position
elseif typeByte == -3 then -- dictionary
local length = readu16(b, position)
position += 2
local dict = {}
for _ = 1, length do
local key, value
key, position = readValue(b, position, ref)
value, position = readValue(b, position, ref)
dict[key] = value
end
return dict, position
elseif typeByte == -4 then -- EnumItem
local length = readi8(b, position)
local value = readstring(b, position + 1, length)
local value2 = readu8(b, position + 1 + length)
return Enum[value]:FromValue(value2), position + 2 + length
elseif typeByte == -5 then -- BrickColor
local value = readi16(b, position)
return BrickColor.new(value), position + 2
elseif typeByte == -6 then -- Enum
local length = readi8(b, position)
local value = readstring(b, position + 1, length)
return Enum[value], position + 1 + length
elseif typeByte == 1 then -- int u8
local value = readu8(b, position)
return value, position + 1
elseif typeByte == 2 then -- int i16
local value = readi16(b, position)
return value, position + 2
elseif typeByte == 3 then -- int i32
local value = readi32(b, position)
return value, position + 4
elseif typeByte == 4 then -- f64
local value = readf64(b, position)
return value, position + 8
elseif typeByte == 5 then -- boolean
local value = readu8(b, position) == 1
return value, position + 1
elseif typeByte == 6 then -- string u8
local length = readu8(b, position)
local value = readstring(b, position + 1, length)
return value, position + length + 1
elseif typeByte == 7 then -- string u16
local length = readu16(b, position)
local value = readstring(b, position + 2, length)
return value, position + length + 2
elseif typeByte == 8 then -- string i32
local length = readi32(b, position)
local value = readstring(b, position + 4, length)
return value, position + length + 4
elseif typeByte == 9 then -- Vector3
local x = readf32(b, position)
local y = readf32(b, position + 4)
local z = readf32(b, position + 8)
return Vector3.new(x, y, z), position + 12
elseif typeByte == 10 then -- Vector2
local x = readf32(b, position)
local y = readf32(b, position + 8)
return Vector2.new(x, y), position + 8
elseif typeByte == 11 then -- CFrame
local components = {}
for i = 1, 12 do
table.insert(components, readf32(b, position + (i - 1) * 4))
end
return CFrame.new(unpack(components)), position + 48
elseif typeByte == 12 then -- Color3
local r = readu8(b, position)
local g = readu8(b, position + 1)
local b = readu8(b, position + 2)
return Color3.fromRGB(r, g, b), position + 3
end
error(`Unsupported type marker: {typeByte}`)
end
function Buffer.new(): Dedicated.DedicatedType
return Dedicated()
end
function Buffer.convert(b: buffer): string
return tostring(b)
end
function Buffer.revert(s: string): buffer
return fromstring(s)
end
function Buffer.write(data: { any }): (buffer, (any)?)
local newBuffer = Dedicated()
newBuffer:pack(data)
return newBuffer:buildAndRemove()
end
function Buffer.read(b: buffer, ref: { any }?): any?
local position = 0
local result = {}
while position < len(b) do
local value
value, position = readValue(b, position, ref)
table.insert(result, value)
end
ref = nil
return table.unpack(result)
end
return Buffer :: typeof(Buffer)

View file

@ -1,4 +1,5 @@
--!strict --!strict
--!optimize 2
return function(): number? return function(): number?
return tonumber(string.sub(tostring(Random.new():NextNumber()), 3, 6)) return tonumber(string.sub(tostring(Random.new():NextNumber()), 3, 8)) -- 6 digits
end end

View file

@ -1,23 +1,54 @@
--!strict --!strict
--!optimize 2
local RateLimit = {} local RateLimit = {}
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local Assert = require(script.Parent.Assert) local Assert = require(script.Parent.Assert)
local Event = require(script.Parent.Parent.Event).Reliable local Events = require(script.Parent.Parent.Event)
local Reliable, Unreliable, Request = Events.Reliable, Events.Unreliable, Events.Request
local Signal = require(script.Parent.Parent.Signal)("Warp_OnSpamSignal")
local map, activity, meta = {}, {}, {}
setmetatable(meta , {
__index = map,
__newindex = function(self, key, value)
if not activity[key] then
activity[key] = os.clock()
end
if (os.clock()-activity[key]) >= 1 then
activity[key] = os.clock()
map[key] = 1
return
end
if value >= 1e2 then -- 100
Signal:Fire(key)
return
end
map[key] = value
end,
})
local function onReceived(player: Player)
if not meta[player] then
meta[player] = 1
return
end
meta[player] += 1
end
function RateLimit.create(Identifier: string, entrance: number?, interval: number?) function RateLimit.create(Identifier: string, entrance: number?, interval: number?)
Assert(typeof(Identifier) == "string", "Identifier must a string type.") Assert(typeof(Identifier) == "string", "Identifier must a string type.")
if RunService:IsServer() then if RunService:IsServer() then
Assert(typeof(entrance) == "number", "entrance must a number type.") Assert(typeof(entrance) == "number", "entrance must a number type.")
Assert(entrance :: number > 0, "entrance must above 0.") Assert(entrance :: number > 0, "entrance must above 0.")
Event:SetAttribute(Identifier.."_ent", entrance) Reliable:SetAttribute(Identifier.."_ent", entrance)
Event:SetAttribute(Identifier.."_int", interval) Reliable:SetAttribute(Identifier.."_int", interval)
else else
while (not Event:GetAttribute(Identifier.."_ent")) or (not Event:GetAttribute(Identifier.."_int")) do while (not Reliable:GetAttribute(Identifier.."_ent")) or (not Reliable:GetAttribute(Identifier.."_int")) do
task.wait(0.5) task.wait(0.1)
end end
entrance = tonumber(Event:GetAttribute(Identifier.."_ent")) entrance = tonumber(Reliable:GetAttribute(Identifier.."_ent"))
interval = tonumber(Event:GetAttribute(Identifier.."_int")) interval = tonumber(Reliable:GetAttribute(Identifier.."_int"))
end end
local entrances: number = 0 local entrances: number = 0
return function(incoming: number?): boolean return function(incoming: number?): boolean
@ -26,9 +57,19 @@ function RateLimit.create(Identifier: string, entrance: number?, interval: numbe
entrances = 0 entrances = 0
end) end)
end end
entrances += incoming and incoming or 1 entrances += incoming or 1
return (entrances <= entrance :: number) return (entrances <= entrance :: number)
end end
end end
function RateLimit.Protect()
if not RunService:IsServer() or Reliable:GetAttribute("Protected") or Unreliable:GetAttribute("Protected") or Request:GetAttribute("Protected") then return end
Reliable:SetAttribute("Protected", true)
Unreliable:SetAttribute("Protected", true)
Request:SetAttribute("Protected", true)
Reliable.OnServerEvent:Connect(onReceived)
Unreliable.OnServerEvent:Connect(onReceived)
Request.OnServerEvent:Connect(onReceived)
end
return RateLimit :: typeof(RateLimit) return RateLimit :: typeof(RateLimit)

View file

@ -1,22 +1,50 @@
--!strict --!strict
--!optimize 2
local SerDes = {}
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
local SerInt = 0 local SerInt = 0
local Event = require(script.Parent.Parent.Event).Reliable local Event = require(script.Parent.Parent.Event).Reliable
local Assert = require(script.Parent.Assert) local Assert = require(script.Parent.Assert)
return function(Identifier: string): string function SerDes.increment(Identifier: string, timeout: number?): number
Assert(typeof(Identifier) == "string", "Identifier must be a string type.") Assert(typeof(Identifier) == "string", "Identifier must be a string type.")
Assert(SerInt < 255, "reached max 255 identifiers.")
if RunService:IsServer() then if RunService:IsServer() then
Assert(SerInt < 255, "reached max 255 identifiers.")
if not Event:GetAttribute(Identifier) then if not Event:GetAttribute(Identifier) then
SerInt += 1 SerInt += 1
Event:SetAttribute(Identifier, string.pack("I1", SerInt)) -- I1 -> 255 max, I2 -> ~ 6.5e4 max. (SerInt) Event:SetAttribute(`{SerInt}`, Identifier)
Event:SetAttribute(Identifier, SerInt)
--Event:SetAttribute(Identifier, string.pack("I1", SerInt)) -- I1 -> 255 max, I2 -> ~ 6.5e4 max. (SerInt), removed/disabled for buffer migration.
end end
else else
while not Event:GetAttribute(Identifier) do local yieldThread: thread = coroutine.running()
task.wait(0.5) local cancel = task.delay(timeout or 10, function() -- yield cancelation (timerout)
task.spawn(yieldThread, nil)
error(`Serdes: {Identifier} is taking too long to retrieve, seems like it's not replicated on server.`, 2)
end)
task.spawn(function()
while coroutine.status(cancel) ~= "dead" and task.wait(0.04) do -- let it loop for yields! 1/24
if Event:GetAttribute(Identifier) then
task.cancel(cancel)
task.spawn(yieldThread, Event:GetAttribute(Identifier))
break
end end
end end
end)
return coroutine.yield() -- yield
end
return Event:GetAttribute(Identifier) return Event:GetAttribute(Identifier)
end end
function SerDes.decrement()
if not RunService:IsServer() or SerInt <= 0 then return end
local Identifier = Event:GetAttribute(`{SerInt}`)
if not Identifier then return end
Event:SetAttribute(`{Identifier}`, nil)
Event:SetAttribute(`{SerInt}`, nil)
SerInt -= 1
Identifier = nil
end
return SerDes :: typeof(SerDes)

View file

@ -1,29 +1,21 @@
--!native --!native
--!strict --!strict
local thread: thread? = nil --!optimize 2
local thread: thread?
local function passer(fn, ...): () local function passer<T...>(func: (T...) -> (), ...: T...): ()
local hold = thread local HoldThread: thread = thread :: thread
thread = nil thread = nil
fn(...) func(...)
thread = hold thread = HoldThread
end end
local function yield(): never local function newThread(): ()
while true do thread = coroutine.running()
passer(coroutine.yield()) while true do passer(coroutine.yield()) end
end
end end
if not thread then return function<T...>(func: (T...) -> (), ...: T...): ()
thread = coroutine.create(yield) if not thread then task.spawn(newThread) end
coroutine.resume(thread :: any, thread) task.spawn(thread :: thread, func, ...)
end
return function(fn: (...any) -> (), ...: any): ()
if not thread then
thread = coroutine.create(yield)
coroutine.resume(thread :: any, thread)
end
task.spawn(thread :: thread, fn, ...)
end end

View file

@ -1,4 +1,6 @@
--!strict --!strict
--!native
--!optimize 2
local Index = {} local Index = {}
local RunService = game:GetService("RunService") local RunService = game:GetService("RunService")
@ -11,6 +13,7 @@ local Client = script.Client
local Type = require(script.Type) local Type = require(script.Type)
local Assert = require(Util.Assert) local Assert = require(Util.Assert)
local Signal = require(script.Signal) local Signal = require(script.Signal)
local Buffer = require(Util.Buffer)
if IsServer then if IsServer then
require(Server.ServerProcess).start() require(Server.ServerProcess).start()
@ -18,39 +21,43 @@ else
require(Client.ClientProcess).start() require(Client.ClientProcess).start()
end end
function Index.Server(Identifier: string, rateLimit: Type.rateLimitArg?): Type.Server function Index.Server(Identifier: string, conf: Type.ServerConf?): Type.Server
Assert(IsServer, `[Warp]: Calling .Server({Identifier}) on client side (expected server side)`) Assert(IsServer, `[Warp]: Calling .Server({Identifier}) on client side (expected server side)`)
Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`) Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`)
return require(Server.Index)(Identifier, rateLimit) :: Type.Server return require(Server.Index)(Identifier, conf) :: Type.Server
end end
function Index.Client(Identifier: string): Type.Client function Index.Client(Identifier: string, conf: Type.ClientConf?): Type.Client
Assert(not IsServer, `[Warp]: Calling .Client({Identifier}) on server side (expected client side)`) Assert(not IsServer, `[Warp]: Calling .Client({Identifier}) on server side (expected client side)`)
Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`) Assert(typeof(Identifier) == "string", `[Warp]: Identifier must be a string type, got {typeof(Identifier)}`)
return require(Client.Index)(Identifier) :: Type.Client return require(Client.Index)(Identifier, conf) :: Type.Client
end end
function Index.fromServerArray(arrays: { any }): Type.fromServerArray function Index.fromServerArray(arrays: { string } | { [string]: Type.ServerConf }): Type.fromServerArray
Assert(IsServer, `[Warp]: Calling .fromServerArray({arrays}) on client side (expected server side)`) Assert(IsServer, `[Warp]: Calling .fromServerArray({arrays}) on client side (expected server side)`)
Assert(typeof(arrays) == "table", "[Warp]: Array must be a table type") Assert(typeof(arrays) == "table", "[Warp]: Array must be a table type, got {typeof(arrays)}")
local copy = {} local copy: { [string]: Type.Server } = {}
for param1: any, param2: any in arrays do for param1, param2: string | Type.ServerConf in arrays do
if typeof(param2) == "table" then if typeof(param2) == "table" then
copy[param1] = Index.Server(param1, param2) copy[param1] = Index.Server(param1, param2)
else else
copy[param2] = Index.Server(param2) copy[param2] = Index.Server(param2)
end end
end end
return table.freeze(copy) :: typeof(copy) return copy
end end
function Index.fromClientArray(arrays: { any }): Type.fromClientArray function Index.fromClientArray(arrays: { string } | { [string]: Type.ClientConf }): Type.fromClientArray
Assert(not IsServer, `[Warp]: Calling .fromClientArray({arrays}) on server side (expected client side)`) Assert(not IsServer, `[Warp]: Calling .fromClientArray({arrays}) on server side (expected client side)`)
Assert(typeof(arrays) == "table", `[Warp]: Array must be a table type, got {typeof(arrays)}`) Assert(typeof(arrays) == "table", `[Warp]: Array must be a table type, got {typeof(arrays)}`)
local copy = {} local copy = {}
for _, identifier: string in arrays do for param1, param2: string | Type.ClientConf in arrays do
copy[identifier] = Index.Client(identifier) if typeof(param2) == "table" then
copy[param1] = Index.Client(param1, param2)
else
copy[param2] = Index.Client(param2)
end end
return table.freeze(copy) :: typeof(copy) end
return copy
end end
function Index.Signal(Identifier: string) function Index.Signal(Identifier: string)
@ -66,4 +73,8 @@ function Index.fromSignalArray(arrays: { any })
return copy :: typeof(copy) return copy :: typeof(copy)
end end
function Index.buffer()
return Buffer.new()
end
return table.freeze(Index) :: typeof(Index) return table.freeze(Index) :: typeof(Index)

View file

@ -1,7 +1,8 @@
-- Warp Library (@Eternity_Devs) -- Warp Library (@Eternity_Devs)
-- version 1.0.5 -- version 1.0.14
--!strict --!strict
--!native --!native
--!optimize 2
local Index = require(script.Index) local Index = require(script.Index)
return { return {
@ -10,6 +11,10 @@ return {
fromServerArray = Index.fromServerArray, fromServerArray = Index.fromServerArray,
fromClientArray = Index.fromClientArray, fromClientArray = Index.fromClientArray,
OnSpamSignal = Index.OnSpamSignal,
Signal = Index.Signal, Signal = Index.Signal,
fromSignalArray = Index.fromSignalArray, fromSignalArray = Index.fromSignalArray,
buffer = Index.buffer,
} }

30
test.project.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "warp-test",
"tree": {
"$className": "DataModel",
"ServerScriptService": {
"$className": "ServerScriptService",
"Test": {
"$path": "test"
}
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Warp": {
"$path": "src"
}
},
"TestService": {
"$className": "TestService",
"$properties": {
"ExecuteWithStudioRun": true
},
"TestEZ": {
"$path": "TestEZ"
},
"run": {
"$path": "runTests.server.luau"
}
}
}
}

166
test/init.spec.luau Normal file
View file

@ -0,0 +1,166 @@
return function()
local Warp = require(game:GetService("ReplicatedStorage").Warp)
describe("Warp.Server", function()
it("should be able to create a new server event", function()
local test = Warp.Server("Test")
expect(test).to.be.ok()
end)
it("should be able to create a new server event with ratelimit configuration", function()
local test = Warp.Server("Test", {
maxEntrance = 10,
interval = 1,
})
expect(test).to.be.ok()
end)
end)
describe("Warp.fromServerArray", function()
it("should be able to create a new server event with arrays", function()
local test = Warp.fromServerArray({
"Test1",
"Test2",
})
expect(test).to.be.ok()
expect(test.Test1).to.be.ok()
expect(test.Test2).to.be.ok()
end)
it("should be able to create a new server event with arrays & ratelimit configuration", function()
local test = Warp.fromServerArray({
"Test1",
"Test2",
["Test3"] = {
maxEntrance = 10,
interval = 0.75,
},
})
expect(test).to.be.ok()
expect(test.Test1).to.be.ok()
expect(test.Test2).to.be.ok()
expect(test.Test3).to.be.ok()
end)
end)
describe("Event.Connect", function()
it("should be able to connect the event", function()
local test = Warp.Server("Test")
test:Connect(function() end)
expect(test.IsConnected).to.be.ok()
end)
end)
describe("Multi Event.Connect", function()
it("should be able to multiple connect the event", function()
local test = Warp.Server("Test")
test:Connect(function() end)
test:Connect(function() end)
expect(test.IsConnected).to.be.ok()
end)
end)
describe("Event.DisconnectAll", function()
it("should be able to disconnect all the event connections", function()
local test = Warp.Server("Test")
test:DisconnectAll()
expect(#test.fn).to.equal(0)
end)
end)
describe("Event.Disconnect", function()
it("should be able to disconnect the event connection", function()
local test = Warp.Server("Test")
local connection = test:Connect(function() end)
expect(test:Disconnect(connection)).to.be.ok()
end)
end)
describe("Warp.Signal", function()
it("should be able to create a new signal", function()
local test = Warp.Signal("Test")
expect(test).to.be.ok()
end)
end)
describe("Warp.fromSignalArray", function()
it("should be able to create a new signal with arrays", function()
local test = Warp.fromSignalArray({
"Test1",
"Test2"
})
expect(test).to.be.ok()
expect(test.Test1).to.be.ok()
expect(test.Test2).to.be.ok()
end)
end)
describe("Signal.Connect", function()
it("should be able to connect the signal", function()
local test = Warp.Signal("Test")
expect(test:Connect(function() end)).to.be.ok()
test:DisconnectAll()
end)
end)
describe("Multi Signal.Connect", function()
it("should be able to multiple connect the signal", function()
local test = Warp.Signal("Test")
expect(test:Connect(function() end)).to.be.ok()
expect(test:Connect(function() end)).to.be.ok()
test:DisconnectAll()
end)
end)
describe("Signal.Fire", function()
it("should be able to fire the signal", function()
local test = Warp.Signal("Test")
test:Once(function(arg)
expect(arg).to.equal("hello world!")
end)
test:Fire("hello world!")
end)
end)
describe("Signal.Invoke", function()
it("should be able to invoke the signal", function()
local test = Warp.Signal("Test")
local connection = test:Connect(function(arg)
if arg ~= "test" then
return
end
return "hello world!"
end)
local receive = test:Invoke(connection, "test")
expect(receive).to.equal("hello world!")
end)
end)
describe("Signal.InvokeTo", function()
it("should be able to invoke to a signal", function()
local test = Warp.Signal("Test")
local test2 = Warp.Signal("Test2")
local connection = test2:Connect(function(arg)
if arg ~= "test" then
return
end
return "hello world!"
end)
local receive = test:InvokeTo("Test2", connection, "test")
expect(receive).to.equal("hello world!")
end)
end)
describe("Signal.Wait", function()
it("should be able to wait for the signal", function()
local test = Warp.Signal("Test")
test:Connect(function() end)
task.spawn(function()
local time = test:Wait()
expect(time).to.be.ok()
expect(time).to.be.a("number")
end)
test:Fire()
end)
end)
end

View file

@ -1,14 +1,10 @@
[package] [package]
name = "imezx/warp" name = "imezx/warp"
version = "1.0.5" version = "1.0.14"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"
exclude = [ exclude = ["node_modules", "docs", ".github", "*.rbxl", "*.rbxmx", "*.rbxml", "*.rbxm", "TestEZ", "test.project.json"]
"node_modules",
"docs",
".github",
]
description = "A very-fast & powerful networking library for Roblox." description = "A very-fast & powerful networking library for Roblox."
[dependencies] [dependencies]