Giter Site home page Giter Site logo

knit's Introduction

Release CI Docs

⚠️ No Longer Maintained ⚠️

Knit has been archived and will no longer receive updates.

Please read here for more information.

Knit

Knit is a lightweight framework for Roblox that simplifies communication between core parts of your game and seamlessly bridges the gap between the server and the client.

Read the documentation for more info.

Install

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 from the Roblox library.
  2. Place Knit directly within ReplicatedStorage.

Wally & Rojo workflow:

  1. Add Knit as a Wally dependency (e.g. Knit = "sleitnick/knit@^1")
  2. Use Rojo to point the Wally packages 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:

local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

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()

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.

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:

local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

-- Create the service:
local MoneyService = Knit.CreateService {
	Name = "MoneyService",
}

-- 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():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:

-- 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:

-- Client-side code
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)
Knit.Start():catch(warn):await()

local MoneyService = Knit.GetService("MoneyService")

MoneyService:GetMoney():andThen(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 networking infrastructure.

knit's People

Contributors

alexasterisk avatar alexwillcode avatar bloeo avatar brinkokevin avatar chanh-t avatar chriscerie avatar encodedvenom avatar enricomfg avatar eqicness avatar evaera avatar howmanysmall avatar jaguar-515 avatar jesse103 avatar joe-rocks avatar kelvenone avatar luarook avatar michael-48 avatar ok-nick avatar osyrisrblx avatar overhash avatar paradoxuum avatar sigmathetatech avatar sleitnick avatar spectrius avatar tkdriverx avatar ukendio avatar undermywheel avatar water261 avatar yuuwa0519 avatar zenthial avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

knit's Issues

Maids should accept promises

For some reason, the GivePromise method was removed from Maid. I don't remember why, but that was a mistake. It should be added back to Maid.

Remotes parented before services complete initialization

The remote objects (e.g. RemoteEvent, RemoteFunction) are parented before services are initialized. This could cause a serious problems if the client tries to access these services beforehand. The client has no way of detecting if a service is fully initialized.

This is a simple fix. The folder where remotes are parented should not be parented until the end of startup in the AeroServer script.

Thread.DelayRepeat should allow execution right away

Currently, Thread.DelayRepeat yields for the given interval before the first execution of the function. However, it would be useful to signal that the function should be called immediately before the first delay.

Current behavior pseudocode:

Thread.DelayRepeat(delayTime, func, ...args):
   repeat:
      wait(delayTime)
      func(...args)

Proposed behavior: Allow a flag to be added to swap the delay and function call:

Thread.DelayRepeat(delayTime, func, immediate, ...args):
   if immediate:
      repeat:
         func(...args)
         wait(delayTime)
   else:
      repeat:
         func(...args)
         wait(delayTime)

Using classes with a service on controller breaks since version 0.7

I originally wrote this issue for the roblox-ts port of Knit, but am posting it here so that the issue can be solved upstream without a hack.

We're currently blocked from upgrading @rbxts/knit past version 0.6 because our "class method" no longer works. There are no errors and Knit reports that everything has started properly, but nothing runs. Since that version, Knit uses TableUtil.Assign() for Knit:CreateService()/Knit:CreateController(), which shallow copies the service/controllers tables and strips any metatable from them.

Our service and controller definitions look like this:

export const ExampleController = Knit.CreateController(
    new (class ExampleController extends KnitController {
        public Name = "ExampleController";

        public KnitStart() {}
    })(),
);

We do this because it is much easier to work in a class rather than an object (which is used by default). In Typescript, there's a lot of advantages to working with a class in this context, rather than an object, such as private members.

The emit of the above code is a generic Lua class, which relies on metatables to function. When the metastable is removed, Knit can't find any methods on it (including KnitInit and KnitStart). Here's the emit on the roblox-ts playground, if you want to inspect it.

Possible memory leak on Signal via args table

When a signal is fired, it inserts the arguments into the _args table with a unique ID. This entry gets removed once the last connected event or thread consumes the event. However, if there are no threads or connections to the event, then the argument entry persists forever.

The fix for this should be pretty straight-forward. If there are no connections or waiting threads, then prevent the event from going forward with firing the event.

The change to the Fire method might look like this:

function Signal:Fire(...)

	-- Stop if no active listeners:
	local totalListeners = (#self._connections + self._threads)
	if (totalListeners == 0) then return end

	local id = self._id
	self._id += 1
	self._args[id] = {totalListeners, {n = select("#", ...), ...}}
	self._threads = 0
	self._bindable:Fire(id)
end

Add Types

Add types to Knit. All libraries and core code should use Luau types.

Use new RunService events

RunService's Heartbeat, RenderStepped, and Stepped events have been deprecated in favor of PostSimulation, PreAnimation, PreRender, and PreSimulation. Any code using the deprecated events needs to be updated with the appropriate new one.

RenderStepped -> PreRender
Stepped -> PreSimulation
Heartbeat -> PostSimulation

The PreAnimation event is new to the render cycle and there is no conversion necessary across the framework.

Error message passed to the call back when Knit fails to start should be a string

Knit.Start():Then(function()
	Debug("Running")

-- Knit fails to start due to some bug in code, below callback runs:

end):Catch(function(errormessage)
	Debug(errormessage) -- error, string expected, got table!
end)

When the code runs and in case Knit fails to start, the errormessage passed isn't a string, and thus results in an error when printing / warning it since it's a table. Instead the error message should be casted to a string via tostring internally and then returned.

The same problem is within the basic example of starting Knit given in the documentation:

-- Load core module:
local Knit = require(game:GetService("ReplicatedStorage").Knit)

----
-- Load services or controllers here
----

-- Start Knit:
Knit.Start():Then(function()
	print("Knit running")
end):Catch(function(err)
	warn(err)
end)

TypeScript / Luau Types Rewrite

This is an exploratory issue. I will attempt to rewrite the framework in TS to see if it's a viable option going forward. If successful, the overall goal would be to use TS as the primary source, and the transpiled Lua version would still be part of the build/distribution.

For those unfamiliar, the awesome roblox-ts project lets developers write TypeScript code that transpiles to Lua specifically for Roblox game development.

Split util modules into monorepo and package them with Wally

With the introduction of Wally (a package manager for Roblox), it is now a good opportunity to split off Knit's utility modules into their own packages. These could exist in a monorepo. Knit would then use Wally to pull in these dependencies.

The plus side to this is more modularity. Developers can pick-and-choose what they want to bring into Knit. This could also be used to split Knit into different feature sets (e.g. wrap all client-facing remote functions with promises by default).

This will be blocked until Wally allows public auth & allows anyone to push packages. Wally is still in its infancy, and so this feature within Knit will have to slowly evolve as Wally evolves.

Re write some portion of the signal modules to support Deferred Lua Event Handling as well as signal usage in Knit

Deferred Lua Event Handling was recently released, and Knit should support it right now or when it comes out of beta. Signals will have undefined and unexpected behaviour and a lot of signal modules have been affected by this change. Knit internally also uses signals, which means it may have been affected by this as well, and I my self am seeing these behaviours when using the custom signal module of Knit.

Create new Comm module

The Comm (communication) module will be a standalone library for creating, invoking, and listening to RemoteFunctions and RemoteEvents. This will be abstracted out of services/controllers, and services/controllers will then utilize this Comm module internally. This will also be useful for users to easily create server/client communication for components.

The nice thing about this module is that it will basically become the low-level base for Knit's purpose in existence. Because it will be standalone, anyone could technically take this module and quickly create their own framework-like system similar to Knit or AGF.

Tutorial & Project Example

Knit needs a tutorial series, where an game project is constructed using Knit. The project will also be open-sourced.

Add Error handling when KnitServer hasnt been started

local servicesFolder = script.Parent:WaitForChild("Services")

KnitClient stops if KnitServer isnt started, this should instead error or add a some other design.
This means if the server isnt started or the serverfolder is destroyed all other clients wont start.

I think it should error instead, and KnitClient.Start() should return a rejected Promise.

Restructure project to fit Rojo 6 recommendations

  • Code should be moved from /src/Knit to /src
  • default.project.json should only include $path to /src so that it can be easily used within other projects
  • Create a test.project.json Rojo config file

Component:WaitFor has potential event leak

The custom timeout process within the WaitFor method doesn't disconnect the added event handler. While this could easily be patched, it makes sense to rewrite to utilize the built-in timeout feature on promises, as well as the from-event feature as well.

A possible solution from TetraDev and OttoHash from the AGF discord server:

function Component:WaitFor(instance, timeout)
    local isName = (type(instance) == "string")

    local function IsInstanceValid(thisObject)
        return ((isName and thisObject._instance.Name == instance) or ((not isName) and thisObject._instance == instance))
    end

    for _,v in ipairs(self._objects) do
        if (IsInstanceValid(v)) then
            return Promise.resolve(v)
        end
    end

    -- We cannot return a value from 'FromEvent', so we must scoot around it.
    local lastObject = nil

    return Promise.FromEvent(self.Added, function(obj)
        lastObject = obj
        return IsInstanceValid(obj)
    end):Then(function()
        return lastObject
    end):Timeout(timeout or 60)
end

Allowing Thread.DelayRepeat to connect immediately rather than yield

Currently with Thread.Delay, the function will always yield for the intervalTime before connecting to the heartbeat. Having the option to connect to the heartbeat immediately would allow the function to be useful in more use-cases.

My proposed solution

function Thread.DelayRepeat(intervalTime, func, yieldMethod, ...)
    local args = table.pack(...)
    local nextExecuteTime
    yieldMethod = yieldMethod or "before"
    assert(yieldMethod == "before" or yieldMethod == "after", "yieldMethod must be \"before\" or \"after\"")
    
    if yieldMethod == "before" then
        nextExecuteTime = (time() + intervalTime)
    elseif yieldMethod == "after" then
        nextExecuteTime = 0
    end

    local hb
    hb = heartbeat:Connect(function()
        if (time() >= nextExecuteTime) then
            nextExecuteTime = (time() + intervalTime)
            func(table.unpack(args, 1, args.n))
        end
    end)
    return hb
end```

Add a DependsOn feature for components

For the sake of good composition, it would be nice if components had a "depends on" feature which would allow developers to indicate if a component must depend on other components to exist first.

For instance, there might be a top-level "Vehicle" component, and other lower-level components (e.g. "Motorcycle") might depend on the Vehicle component. Currently, a lot of custom code has to be written to make this guarantee. This is not ideal, and there should be a built-in feature to accommodate for this need.

For instance, components could have an optional DependsOn table which would list out the components that must exist for itself to instantiate:

local Motorcycle = {}
Motorcycle.__index = Motorcycle

-- Depend on the Vehicle component existing:
Motorcycle.DependsOn = {"Vehicle"}

function Motorcycle.new(instance)
	local self = setmetatable({}, Motorcycle)

	-- We can now guarantee this will return the Vehicle component:
	self.Vehicle = Component.FromTag("Vehicle"):GetFromInstance(instance)

	return self
end
...

Rules:

  1. Only when all of the DependsOn components are available will the component be instantiated.
  2. If any of the DependsOn components become unavailable, the component will be destroyed.

Make cleaner logo

The current logo was actually made in 3D at the beginning of Knit and was just supposed to be a placeholder. I'd like to make a cleaner version with the same aspects: A tilted square with some sort of reference to knitting.

Add Signal.Proxy constructor

Sometimes it is nice to wrap an existing RBXScriptSignal object. By creating a custom Signal that wraps an RBXScriptSignal, the custom signal could easily be cleaned up to disconnect all connections to that real signal.

A use would look like this:

local TouchTap = Signal.Proxy(UserInputService.TouchTap)
TouchTap:Connect(function() ... end)
TouchTap:Destroy()

In that example, TouchTap:Destroy() would also destroy the connected function above it. This is useful if the proxy is going to be exposed to other code (e.g. as a field of a class object).

The implementation of this constructor is quite simple:

function Signal.Proxy(rbxSignal, maid)
	assert(typeof(rbxSignal) == "RBXScriptSignal", "Argument #1 must be of type RBXScriptSignal")
	local signal = Signal.new(maid)
	signal._rbxSignalHandle = rbxSignal:Connect(function(...)
		signal:Fire(...)
	end)
	return signal
end

-- Additional cleanup necessary in Destroy method:
function Signal:Destroy()
	...
	if (self._rbxSignalHandle) then
		self._rbxSignalHandle:Disconnect()
	end
end

The README example adds no value

The little bit of Knit code on the README page adds no value. It only shows how to start Knit, but doesn't highlight any key features or benefits of Knit.

This should either be removed or more meaningful code should be included.

TableUtil.Sync will remove arrays from the original template

When using TableUtil.Sync, any empty arrays in the template data will override any arrays in the original template.

Reproduction

local function Sync(tbl, templateTbl) -- this function is coped from the TableUtil file

    assert(type(tbl) == "table", "First argument must be a table")
    assert(type(templateTbl) == "table", "Second argument must be a table")
    
    -- If 'tbl' has something 'templateTbl' doesn't, then remove it from 'tbl'
    -- If 'tbl' has something of a different type than 'templateTbl', copy from 'templateTbl'
    -- If 'templateTbl' has something 'tbl' doesn't, then add it to 'tbl'
    for k,v in pairs(tbl) do
        
        local vTemplate = templateTbl[k]
        
        -- Remove keys not within template:
        if (vTemplate == nil) then
            tbl[k] = nil
            
        -- Synchronize data types:
        elseif (type(v) ~= type(vTemplate)) then
            if (type(vTemplate) == "table") then
                tbl[k] = CopyTable(vTemplate)
            else
                tbl[k] = vTemplate
            end
        
        -- Synchronize sub-tables:
        elseif (type(v) == "table") then
            Sync(v, vTemplate)
        end
        
    end
    
    -- Add any missing keys:
    for k,vTemplate in pairs(templateTbl) do
        
        local v = tbl[k]
        
        if (v == nil) then
            if (type(vTemplate) == "table") then
                tbl[k] = CopyTable(vTemplate)
            else
                tbl[k] = vTemplate
            end
        end
        
    end
    
end

local t1 = { Stamps = {"Level-1"} }
local t2 = { Stamps = {} }
print(t1) -- notice how t1 has Level-1 in it here
Sync(t1, t2)
print(t1) -- and it now no longer has Level-1

Knit/Util/Component.lua assumes Knit located at ReplicatedStorage.Knit

local Knit = require(game:GetService("ReplicatedStorage").Knit)
local Maid = require(Knit.Util.Maid)
local Signal = require(Knit.Util.Signal)
local Promise = require(Knit.Util.Promise)
local Thread = require(Knit.Util.Thread)
local TableUtil = require(Knit.Util.TableUtil)

Should be:

local Maid = require(script.Parent.Maid)
local Signal = require(script.Parent.Signal)
local Promise = require(script.Parent.Promise)
local Thread = require(script.Parent.Thread)
local TableUtil = require(script.Parent.TableUtil)

Component ServerID should be set as an attribute instead of StringValue

Components get marked with a ServerID with a StringValue. With the release of Attributes, this should be switched to use an attribute value instead. Attributes are faster for read/write and also replicate with the instance itself.

It might be worth changing the name to something more unique too, such as ComponentServerId.

Unit Tests

Unit tests for all (or at least most) utility modules and core features of Knit.

Split up RemoteSignal and RemoteProperty into separate server/client modules

RemoteSignal and RemoteProperty exploit Lua's dynamic capabilities to dynamically switch behavior based on the current runtime environment (server vs. client). This is cool, but a complete headache for Luau and TypeScript types. These should be split up into separate modules.

The con to this is verbosity, but it is probably worth it in the long-run.

The first two .Tick events fire at the same time.

The first two .Tick events fire at the same time. Tested on a blank baseplate. Script in ServerScriptStorage.

Code:

local knit = require(game.ReplicatedStorage.Knit)
local timer = require(knit.Util.Timer)

local myTimer = timer.new(1)
myTimer.Tick:Connect(function()
	print("Tock")
end)

wait(5)
myTimer:Start()

Output:

Tock (x2)
Tock (x3)
...

Investigate if component runtime updaters should be independent per object

Component runtime updaters (e.g. HeartbeatUpdate; soon to be PostSimulationUpdate) are currently updated in a loop per component instance. Instead, this should probably be done per object. An investigation should be done to benchmark both options and find the best balance.

If the result is minuscule in terms of performance gain, then this will be dropped. Should be 10%+ performance increase to justify any change.

Remove unnecessary parentheses

As a change of coding style, unnecessary parentheses around conditionals should be removed. This is to keep in step with coding styles across Roblox's open-source ecosystem.

e.g. this:

if (this) then

...will become:

if this then

There are obviously still times with parentheses around conditionals are necessary, but any unnecessary occurrences should be removed.

Component module is unstable for StreamingEnabled games

The Component module is unstable for StreamingEnabled games.

A common practice is to attach a Component to a model so that the model can be given different behavior, including behavior to its children (which becomes the problem). This works fine on the server, as all parts are immediately available.

However, this does not work as expected on the client. Models don't stream in/out like parts do. Therefore, a client Component could be initialized even though its children parts haven't (or might not ever) streamed in. One could fix this by adding a WaitForChild; however, this is problematic: Yielding within a component is bad because the component might end up being inactive by the time WaitForChild has completed or timed out. Thus more code has to be written to check the component state (e.g. you could check for self._destroyed which is set internally when the component instance goes away).

This still fails to address the problem of parts streaming out and then back in though. Therefore, a more robust solution is necessary to deal with this problem.


It is possible that the solution is not to modify the Component module, but rather add a new Observer module that can be used within components to watch certain parts.

circular dependencies on module collections yields without an error or warning

I was under the impression that exposing modules to knit using the recommended method as such would help us avoid circular dependency issues beween modules that require each other. I believe Aero solved this problem.

recommended example:
Knit.MyModules = ReplicatedStorage.MyModules

However, when I have two modules that require each other, Knit yields and does not show any error or warning, making an old Roblox gotcha even harder to track down than before.

It would be nice if these circular dependencies were not an issue, such as with Nevermore and in the case that's not possible, a warning or error of some sort to help us track down the issue.

Allow RemoteProperties to target specific clients

RemoteProperties are a great utility, but their actual application is limited by its replication behavior. As it stands, RemoteProperties are created on the server and exposed to every client, which makes them perfect for small and global systems; But on a large social platform where multiplayer is the name of the game, they're largely redundant.

A RemoteProperty that only faces one or any number of specified clients would be really helpful. There have been many occasions in the past where I've fully intended to use RemoteProperties but by the implementation stage, the problem is just complicated enough that a RemoteEvent/Function just makes more sense.

In my eyes, this addition would make the applications of RemoteProperties much wider and more versatile.

Knit Component should call Removed and destroy component object when parented outside of whitelist

Currently, components are only removed when the CollectionService says they are removed. However, this might not match with the whitelist for component descendants. For instance, if a component is reparented to Lighting, it will not be destroyed even though Lighting is not whitelisted.

This can be fixed by watching the parent of the component (instance:GetPropertyChanged("Parent"):Connect(...)) and cleaning up the component if the new parent is not whitelisted.

Upgrade to roblox-lua-promise v3.0.1

GHSA-mq7h-vf5c-52f4

Impact

Promise.delay's internal thread scheduler has a bug in v3.0.0 that can cause Promise.delay to error which can interrupt the Promise chain with errors escaping the Promise library.

Promise.is would return false for Promises created with roblox-lua-promise@v2 which would break some APIs when passing Promises to things like Promise.all.

Patches

Upgrade to version v3.0.1 which fixes these issues.

Workarounds

There are no good ways to work around these issues.

Fix docs auto-deployment code highlighting issue

When the GitHub action for deploying docs is run, code highlighting doesn't work. This could be either a versioning issue or missing dependencies. Not entirely sure.

Of course, "it works on my machine" falls into play here. Docs can be successfully deployed if I run the docs.sh deploy command locally. So the problem is most likely versioning or dependency related. Most likely, the requirements.txt file needs to be updated to fix this problem.

Component instance should not be marked as private

Right now, a component's instance is set to _instance -- the underscore indicating it is a private field. It is useful to reference the instance outside of the component though. This field should be changed to Instance. So you could do myComponent.Instance instead.

Knit CLI

A simple CLI tool for Knit to help assist in:

  1. Project setup
  2. Check for updates
  3. TBD

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.