Skip to content
This repository has been archived by the owner on Jul 31, 2024. It is now read-only.

Commit

Permalink
Merge pull request #147 from AtollStudios/v0.0.22-alpha
Browse files Browse the repository at this point in the history
v0.0.22-alpha
  • Loading branch information
Sleitnick authored Aug 25, 2021
2 parents bd05d9e + b2a54fc commit d8b07ec
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 73 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.0.22-alpha

- Fix `TableUtil.Sample` algorithm to properly implement a partial Fisher-Yates shuffle
- Fix `TableUtil.Reduce` to handle the `init` parameter properly
- Update Janitor to [v1.13.6](https://github.com/howmanysmall/Janitor/releases/tag/V1.13.6)
- Small documentation adjustments

## 0.0.21-alpha

- Fix issue with having multiple required components
Expand Down
102 changes: 79 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,98 @@ Knit is still in alpha, but will soon be elevated to beta. See the [Beta Roadmap

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

## Example
## Install

Here is a simple and fully-working example where a PointsService is created server-side and lets the client access points from the service. No RemoteFunctions or RemoteEvents have to be made; those are handled internally by Knit.
Installing Knit is very simple. Just drop the module into ReplicatedStorage. Knit can also be used within a Rojo project.

**Roblox Studio workflow:**

1. Get [Knit](https://www.roblox.com/library/5530714855/Knit) from the Roblox library.
1. Place Knit directly within ReplicatedStorage.

**Rojo workflow:**

1. [Download Knit](https://github.com/AtollStudios/Knit/releases/latest/download/knit.zip) from the latest release on GitHub.
1. Extract the Knit directory from the zipped file.
1. Place Knit within your project.
1. Use Rojo to point Knit to ReplicatedStorage.

## Basic Usage

The core usage of Knit is the same from the server and the client. The general pattern is to create a single script on the server and a single script on the client. These scripts will load Knit, create services/controllers, and then start Knit.

The most basic usage would look as such:

**Server:**
```lua
local Knit = require(game:GetService("ReplicatedStorage").Knit)

-- Create a PointsService:
local PointsService = Knit.CreateService {
Name = "PointsService";
Client = {};
}
Knit.Start():Catch(warn)
-- Knit.Start() returns a Promise, so we are catching any errors and feeding it to the built-in 'warn' function
-- You could also chain 'Await()' to the end to yield until the whole sequence is completed:
-- Knit.Start():Catch(warn):Await()
```

-- Expose an endpoint that the client can invoke:
function PointsService.Client:GetPoints(player)
return 10
end
That would be the necessary code on both the server and the client. However, nothing interesting is going to happen. Let's dive into some more examples.

Knit.Start()
```
### A Simple Service

A service is simply a structure that _serves_ some specific purpose. For instance, a game might have a MoneyService, which manages in-game currency for players. Let's look at a simple example:

**Client:**
```lua
local Knit = require(game:GetService("ReplicatedStorage").Knit)

local MyController = Knit.CreateController {
Name = "MyController";
-- Create the service:
local MoneyService = Knit.CreateService {
Name = "MoneyService";
}

function MyController:KnitStart()
-- Fetch points from the server-side PointsService:
local PointsService = Knit.GetService("PointsService")
local points = PointsService:GetPoints()
print("Points", points)
-- Add some methods to the service:

function MoneyService:GetMoney(player)
-- Do some sort of data fetch
local money = someDataStore:GetAsync("money")
return money
end

function MoneyService:GiveMoney(player, amount)
-- Do some sort of data fetch
local money = self:GetMoney(player)
money += amount
someDataStore:SetAsync("money", money)
end

Knit.Start()
Knit.Start():Catch(warn)
```

Now we have a little MoneyService that can get and give money to a player. However, only the server can use this at the moment. What if we want clients to fetch how much money they have? To do this, we have to create some client-side code to consume our service. We _could_ create a controller, but it's not necessary for this example.

First, we need to expose a method to the client. We can do this by writing methods on the service's Client table:

```lua
-- Money service on the server
...
function MoneyService.Client:GetMoney(player)
-- We already wrote this method, so we can just call the other one.
-- 'self.Server' will reference back to the root MoneyService.
return self.Server:GetMoney(player)
end
...
```

We can write client-side code to fetch money from the service:

```lua
-- Client-side code
local Knit = require(game:GetService("ReplicatedStorage").Knit)
Knit.Start():Catch(warn):Await()

local moneyService = Knit.GetService("MoneyService")
local money = moneyService:GetMoney()

-- Alternatively, using promises:
moneyService:GetMoneyPromise():Then(function(money)
print(money)
end)
```

Under the hood, Knit is creating a RemoteFunction bound to the service's GetMoney method. Knit keeps RemoteFunctions and RemoteEvents out of the way so that developers can focus on writing code and not building communication infrastructure.
2 changes: 0 additions & 2 deletions docs/gettingstarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ Installing Knit is very simple. Just drop the module into ReplicatedStorage. Kni
1. Place Knit within your project.
1. Use Rojo to point Knit to ReplicatedStorage.

Please note that it is vital for Knit to live directly within ReplicatedStorage. It cannot be nested in another instance, nor can it live in another service. This is due to other parts of Knit needing to reference back to the Knit module.

## Basic Usage

The core usage of Knit is the same from the server and the client. The general pattern is to create a single script on the server and a single script on the client. These scripts will load Knit, create services/controllers, and then start Knit.
Expand Down
2 changes: 2 additions & 0 deletions docs/styleguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ All source files should follow a similar format to this template:
-- Return module
```

In other Roblox programming ecosystems, it is usually standard for service refs to come _before_ module requires. The reason for the switch here is that imports are always first in just about every other ecosystem, and thus Knit tries to follow the more global standard.

Example of `MyModule.lua`:

```lua
Expand Down
2 changes: 1 addition & 1 deletion docs/util/option.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
An [Option](https://github.com/AtollStudios/Knit/blob/main/src/Util/Option.lua) is a powerful concept taken from [Rust](https://doc.rust-lang.org/std/option/index.html) and other languages. The purpose is to represent an optional value. An option can either be `Some` or `None`. Using Options helps reduce `nil` bugs (which can cause silent bugs that can be hard to track down). Options automatically serialize/deserialize across the server/client boundary when passed through services or controllers.

For full documentation, check out the [LuaOption](https://github.com/AtollStudios/LuaOption) repository.
For full documentation, check out the [LuaOption](https://github.com/Sleitnick/LuaOption) repository.

Using Options is very simple:

Expand Down
2 changes: 1 addition & 1 deletion docs/util/streamable.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Streamables can be paired with Components. If a component is attached to a model and the component needs to access the model's children, a streamable can guarantee safe access to those children. When using a streamable within a component, be sure to pass the streamable to the component's janitor for automatic cleanup.

Check out Roblox's [Content Streaming](https://developer.roblox.com/en-us/articles/content-streaming) developer documentation for more information on how content is streamed into and out of games during runtime.
Check out Roblox's [Content Streaming](https://developer.roblox.com/en-us/articles/content-streaming) documentation for more information on how content is streamed into and out of games during runtime.

```lua
local Streamable = require(Knit.Util.Streamable)
Expand Down
46 changes: 44 additions & 2 deletions docs/util/tableutil.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,11 @@ print(scores) --> {23, 40, 31}
---------------

## `Reduce`
`TableUtil.Reduce(tbl: table, callback: (acc: number, val: number) -> number [, init: number]): number`
`TableUtil.Reduce(tbl: table, callback: (acc: any, val: any) -> number [, init: any]): any`

Reduces the contents of a table to a number.
Reduces the contents of a table to a certain value.

If the `init` value is not specified, it defaults to the first value in the table.

```lua
local scores = {10, 20, 30}
Expand All @@ -200,6 +202,32 @@ end, initialValue)
print(totalScore) --> 100
```

The `Reduce` function is not limited to numbers. Here's an example of a very inefficient string builder:
```lua
local values = {"A", "B", "C"}
local str = TableUtil.Reduce(values, function(accumulator, value)
return accumulator .. value
end)

print(str) --> "ABC"
```

Functions could also be combined using a reducer:
```lua
local function Square(x) return x * x end
local function Double(x) return x * 2 end

local Func = TableUtil.Reduce({Square, Double}, function(a, b)
return function(x)
return a(b(x))
end
end)
-- Func == Square(Double(x))

local result = Func(10)
print(result) --> 400
```

`Reduce` can be used with both arrays and dictionaries.

---------------
Expand Down Expand Up @@ -445,6 +473,20 @@ print("SomeBelowFive", someBelowFive) --> SomeBelowFive, false

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

## `Truncate`
`TableUtil.Truncate(tbl: table, length: number): table`

Truncates a table to the specified length.

```lua
local t1 = {10, 20, 30, 40, 50}
local t2 = TableUtil.Truncate(t1, 3)

print(t2) --> {10, 20, 30}
```

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

## `Zip`
`TableUtil.Zip(...table): Iterator`

Expand Down
68 changes: 40 additions & 28 deletions src/Util/Janitor.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,37 @@ end
local NOT_A_PROMISE = "Invalid argument #1 to 'Janitor:AddPromise' (Promise expected, got %s (%s))"
local METHOD_NOT_FOUND_ERROR = "Object %s doesn't have method %s, are you sure you want to add it? Traceback: %s"

local Janitor = {
ClassName = "Janitor";
__index = {
CurrentlyCleaning = true;
[IndicesReference] = nil;
};
}
local Janitor = {}
Janitor.ClassName = "Janitor"
Janitor.__index = {}

Janitor.__index.CurrentlyCleaning = true
Janitor.__index[IndicesReference] = nil

local TypeDefaults = {
["function"] = true;
RBXScriptConnection = "Disconnect";
}

--[[**
Instantiates a new Janitor object.
@returns [t:Janitor]
**--]]
function Janitor.new()
return setmetatable({
CurrentlyCleaning = false;
[IndicesReference] = nil;
}, Janitor)
end

--[[**
Determines if the passed object is a Janitor.
@param [t:any] Object The object you are checking.
@returns [t:boolean] Whether or not the object is a Janitor.
**--]]
function Janitor.Is(Object)
function Janitor.Is(Object: any): boolean
return type(Object) == "table" and getmetatable(Object) == Janitor
end

type StringOrTrue = string | boolean

--[[**
Adds an `Object` to Janitor for later cleanup, where `MethodName` is the key of the method within `Object` which should be called at cleanup time. If the `MethodName` is `true` the `Object` itself will be called instead. If passed an index it will occupy a namespace which can be `Remove()`d or overwritten. Returns the `Object`.
@param [t:any] Object The object you want to clean up.
@param [t:string|true?] MethodName The name of the method that will be used to clean up. If not passed, it will first check if the object's type exists in TypeDefaults, and if that doesn't exist, it assumes `Destroy`.
@param [t:any?] Index The index that can be used to clean up the object manually.
@returns [t:any] The object that was passed as the first argument.
**--]]
function Janitor.__index:Add(Object, MethodName, Index)
function Janitor.__index:Add(Object: any, MethodName: StringOrTrue?, Index: any?): any
if Index then
self:Remove(Index)

Expand Down Expand Up @@ -108,7 +98,7 @@ end
@param [t:any] Index The index you want to remove.
@returns [t:Janitor] The same janitor, for chaining reasons.
**--]]
function Janitor.__index:Remove(Index)
function Janitor.__index:Remove(Index: any): Janitor
local This = self[IndicesReference]

if This then
Expand Down Expand Up @@ -142,7 +132,7 @@ end
@param [t:any] Index The index that the object is stored under.
@returns [t:any?] This will return the object if it is found, but it won't return anything if it doesn't exist.
**--]]
function Janitor.__index:Get(Index)
function Janitor.__index:Get(Index: any): any?
local This = self[IndicesReference]
if This then
return This[Index]
Expand All @@ -164,11 +154,11 @@ function Janitor.__index:Cleanup()
end

if MethodName == true then
Object()
task.spawn(Object)
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
ObjectMethod(Object)
task.spawn(ObjectMethod, Object)
end
end

Expand Down Expand Up @@ -204,26 +194,36 @@ Janitor.__call = Janitor.__index.Cleanup
-- @param Instance Instance The Instance the Janitor will wait for to be Destroyed
-- @returns Disconnectable table to stop Janitor from being cleaned up upon Instance Destroy (automatically cleaned up by Janitor, btw)
-- @author Corecii
local Disconnect = {Connected = true}
local Disconnect = {}
Disconnect.Connected = true
Disconnect.__index = Disconnect

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

function Disconnect._new(RBXScriptConnection: RBXScriptConnection)
return setmetatable({
Connection = RBXScriptConnection;
}, Disconnect)
end

function Disconnect:__tostring()
return "Disconnect<" .. tostring(self.Connected) .. ">"
end

type RbxScriptConnection = typeof(Disconnect._new(game:GetPropertyChangedSignal("ClassName"):Connect(function() end)))

--[[**
"Links" this Janitor to an Instance, such that the Janitor will `Cleanup` when the Instance is `Destroyed()` and garbage collected. A Janitor may only be linked to one instance at a time, unless `AllowMultiple` is true. When called with a truthy `AllowMultiple` parameter, the Janitor will "link" the Instance without overwriting any previous links, and will also not be overwritable. When called with a falsy `AllowMultiple` parameter, the Janitor will overwrite the previous link which was also called with a falsy `AllowMultiple` parameter, if applicable.
@param [t:Instance] Object The instance you want to link the Janitor to.
@param [t:boolean?] AllowMultiple Whether or not to allow multiple links on the same Janitor.
@returns [t:RbxScriptConnection] A pseudo RBXScriptConnection that can be disconnected to prevent the cleanup of LinkToInstance.
**--]]
function Janitor.__index:LinkToInstance(Object, AllowMultiple)
function Janitor.__index:LinkToInstance(Object: Instance, AllowMultiple: boolean?): RbxScriptConnection
local Connection
local IndexToUse = AllowMultiple and newproxy(false) or LinkToInstanceIndex
local IsNilParented = Object.Parent == nil
Expand Down Expand Up @@ -261,7 +261,7 @@ function Janitor.__index:LinkToInstance(Object, AllowMultiple)
ChangedFunction(nil, Object.Parent)
end

Object = nil
Object = nil :: any
return self:Add(ManualDisconnect, "Disconnect", IndexToUse)
end

Expand All @@ -270,7 +270,7 @@ end
@param [t:...Instance] ... All the instances you want linked.
@returns [t:Janitor] A new janitor that can be used to manually disconnect all LinkToInstances.
**--]]
function Janitor.__index:LinkToInstances(...)
function Janitor.__index:LinkToInstances(...: Instance): Janitor
local ManualCleanup = Janitor.new()
for _, Object in ipairs({...}) do
ManualCleanup:Add(self:LinkToInstance(Object, true), "Disconnect")
Expand All @@ -279,4 +279,16 @@ function Janitor.__index:LinkToInstances(...)
return ManualCleanup
end

--[[**
Instantiates a new Janitor object.
@returns [t:Janitor]
**--]]
function Janitor.new()
return setmetatable({
CurrentlyCleaning = false;
[IndicesReference] = nil;
}, Janitor)
end

export type Janitor = typeof(Janitor.new())
return Janitor
Loading

0 comments on commit d8b07ec

Please sign in to comment.