Giter VIP home page Giter VIP logo

kemal-session's People

Contributors

crisward avatar da99 avatar daliborfilus avatar drum445 avatar felipeelias avatar jasonl99 avatar k-nrs avatar kefahi avatar mamantoha avatar mang avatar marzhaev avatar neovintage avatar rdp avatar sdogruyol avatar sija avatar thyra avatar tobyapi avatar ukd1 avatar vanhecke 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

kemal-session's Issues

Crystal 0.30.0 Depreciated URI.unescape warning

I got this warning while compiling a Kemal app using

crystal build src/gaslogcr.cr -o bin/gaslogcr --release --warnings all --error-on-warnings

In /usr/share/crystal/src/uri/encoding.cr:116:3

 116 | def self.unescape(string : String, plus_to_space = false)
       ^-------
Warning: Deprecated URI.unescape. Use .decode or .decode_www_form instead

Grepping through my lib/ directory, I found lib/kemal-session/src/kemal-session/base.cr line 17 makes a call to URI.unescape. As a quick workaround, I changed .unescape to .decode and tested. It compiled with no errors.

As this is just a warning, I'm not real concerned with my code not working yet. Just thought I would get it on record. Listed as a breaking change in Crystal 0.30.0 changelog.

Thanks.

Multiple Storable Objects

If I set multiple storable objects, the object type seems to get mismatched. This seems to happen when the data gets read back from a file, and it parsed with .from_json.

Let me know if you need more info to recreate.

Error: undefined method 'name' for UserStorableObject

Hi,

I don't know if this is a bug or just an incomplete example issue but I was trying to add UserStorableObject to my project but following the example in the README gave me Error: undefined method 'name' for UserStorableObject.

It seems like the example is missing a couple of properties in the UserStorableObject or did I miss something?

require "json"
require "kemal"
require "kemal-session"

Kemal::Session.config do |config|
  config.secret = "some_secret"
end

class UserStorableObject
  include JSON::Serializable
  include Kemal::Session::StorableObject

  # An error is raised without these lines
  # property id : Int32
  # property name : String

  def initialize(@id : Int32, @name : String); end
end

get "/set" do |env|
  user = UserStorableObject.new(123, "charlie")
  env.session.object("user", user)
end

get "/get" do |env|
  user = env.session.object("user").as(UserStorableObject)
  "The user stored in session is #{user.name}"
end

Kemal.run

Spawn garbage collector only once

Right now there is some chaos with the garbage collector: You spawn a gc fiber every time a Session object is created. That happens at least once every HTTP request. So after a short time, you will have hundreds of gc fibers running.
Additionally, the memory engine sleeps in its run_gc method on line 50. So now the gc interval is twice as long as the user intends.

My initial solution looked like this [1]:

# @TODO would it be better to not wrap this inside the class?
# What difference does it make?

class Session

  spawn do
    loop do
      Session.config.engine.run_gc
      sleep(Session.config.gc_interval.total_seconds)
    end
  end

end

It is probably not perfect but it makes sure that there is only one instance running independently of how many sessions are active. I also like its simplicity (instead of using a singleton pattern for example).
But maybe someone has a better idea?

Crystal 0.27.0 breaking Time methods

I've been getting errors like undefined method 'epoch_ms' for Time while attempting to use this shard. I have kemal-session at v0.10.0, as it throws an error when I try updating to v0.11.0.
Error resolving kemal-session (0.11.0, 0.10.0)

Error upgrading

From what I can tell:

include Kemal::Session::StorableObject
^
Error: StorableObject User needs to define to_json

meant "my earlier JSON.mapping macro wasn't working since upgrading to crystal 1.x anymore" make that work first (can comment out this line to see real failure message).

Empty Sessions

I noticed new 'empty' session objects get saved. As sessions are typically saved for an hour these can soon mount up, especially from clients which don't retain cookies (ie bots / webcrawlers etc).

I think it'd be a good idea if the various engines or the core library could prevent the saving of empty sessions, thoughts?

Kemal with newest crystal versions not working

The error is:

Warning: Zlib is deprecated, use Compress::Zlib
Showing last frame. Use --error-trace for full trace.

In lib/kemal/src/kemal/helpers/helpers.cr:143:7

 143 | Gzip::Writer.open(env.response) do |deflate|
       ^
Error: undefined constant Gzip::Writer

The fix is to change the Gzip prefix to Compress::Zlib::Writer

Also the Gzip::deflate should be Compress::Deflate::Writer

Crystal 0.27.0 breaking change - Time

I ran into errors like this involving Time#epoch:

Error in lib/kemal-session/src/kemal-session/engines/memory.cr:52: expanding macro 
    defne_storage
undefined method 'epoch_ms' for Time

I have managed to fix these errors in my local copy as long as I don't update kemal-session. Since I am not a professional developer (newb), I hesitate to do a fork and pull request.

Refererce Crystal breaking change #6662

Thank you.

Signing Cookies

I couldn't tell. Are the values being signed before being shipped off?

Nil value error

Hello guys
I am very new to crystal and I stucked at something si mple.

I am using Crecto and coding a simple website. All I want to do is store user_id variable in session.

changeset = Repo.insert(new_user)
if changeset.valid? == false
  puts "Changeset is invalid"
  env.response.status_code = 500
else
  puts "\nThe item was successfully ADDED. ID:#{changeset.instance.id}\n"
  #Here-----> env.session.int("user_id", changeset.instance.id)
  resp(env, {"message": "Success.", "state": true})
end

Getting this error:

no overload matches 'Kemal::Session#int' with types String, (Int32 | Nil)
Overloads are:
 - Kemal::Session#int(k : String, v : Int32)
 - Kemal::Session#int(k : String)
Couldn't find overloads for these types:
 - Kemal::Session#int(String, Nil)

        env.session.int("user_id", changeset.instance.id)

It says changeset.instance.id is nil but it is not nil. I really wonder the reason.
Thanks in advance.

Fails to compile on Crystal 1.0.0

In lib/kemal-session/src/kemal-session/engines/memory.cr:70:16

 70 | @store.delete_if do |id, entry|
             ^--------
Error: undefined method 'delete_if' for Hash(String, String)

Despite of also using json_mapping

Generally methods ending in ? return bool

Sdogruyol, thanks for making the 'killer app' of crystal!

The syntax of .int?("blah") below is a bit confusing. Why is the ? necessary?
Generally when I see a question sign, I am expecting a boolean return.

get "/get" do |env|
  num  = env.session.int("number") # get the value of "number"
  env.session.int?("hello") # get value or nil, like []?
  "Value of random number is #{num}."
end

Rename Session class

I added kemal-csrf to our project and as you know, it depends on this shard. This broke our application since it also had a class named Session. It wasn't quite obvious right away what the problem was when the code didn't compile. Anyway, I was able to fix the issue by renaming our Session class.

So how about if the Session class in this library would be either renamed to KemalSession or Kemal::Session? It would be quite an easy change to prevent similar issues in the other projects.

Deprecated JSON.mapping was dropped in crystal 0.36.0

Used by kemal-session in MemoryEngine and FileEngine:

JSON.mapping({
{% for name, type in vars %}
{{name.id}}s: Hash(String, {{type}}),
{% end %}
last_access_at: Int64,
id: String,
})

JSON.mapping({
{% for name, type in vars %}
{{name.id}}s: Hash(String, {{type}}),
{% end %}
})

As per the release notes, JSON::Serializable should be used instead, or crystal-lang/json_mapping.cr as a temporary fix

ability to remove from session

I did not see a "delete_int" option for instance, might be nice to be able to add as well as remove things from the sessions :)

0.33 compat

Today:

There was a problem expanding macro 'define_storage'

Code in lib/kemal-session/src/kemal-session/engines/memory.cr:52:9

 52 | define_storage({
      ^
Called macro defined in lib/kemal-session/src/kemal-session/engines/memory.cr:7:9

 7 | macro define_storage(vars)

Which expanded to:

 > 22 |         
 > 23 |           @ints = Hash(String, Int32).new
 > 24 |           @last_access_at = Time.new.to_unix_ms
                                         ^--
Error: no overload matches 'Time.new'

`Abstract must be implemented by Session::RedisEngine` Error

Error Message:

Error in lib/kemal-session/src/kemal-session/engine.cr:81: expanding macro

  abstract_engine({
  ^

in macro 'abstract_engine' lib/kemal-session/src/kemal-session/engine.cr:4, line 75:

.......

> 75.         abstract def object(session_id : String, k : String, v : Session::StorableObject::StorableObjectContainer)

.......

abstract `def Session::Engine#object(session_id : String, k : String, v : Session::StorableObject::StorableObjectContainer)` must be implemented by Session::RedisEngine

Yes I am using neovitage/kemal-session-redis but there are some issues so I use progdo/kemal-session-redis. Whether I am using mine or neovitage`s, there is this error.

@sdogruyol @neovintage

Error: no overload matches 'Time.new'

There was a problem expanding macro 'define_storage'

Code in lib/kemal-session/src/kemal-session/engines/memory.cr:52:9

 52 | define_storage({
      ^
Called macro defined in lib/kemal-session/src/kemal-session/engines/memory.cr:7:9

 7 | macro define_storage(vars)

Which expanded to:

 > 22 |
 > 23 |           @ints = Hash(String, Int32).new
 > 24 |           @last_access_at = Time.new.to_unix_ms
                                         ^--
Error: no overload matches 'Time.new'

Overloads are:
 - Time.new(time : LibC::Timespec, location : Location = Location.local)
 - Time.new(pull : JSON::PullParser)
 - Time.new(*, seconds : Int64, nanoseconds : Int32, location : Location)
 - Time.new(*, unsafe_utc_seconds : Int64)

Crystal: 0.34.0

What is the license of this project?

The project is missing a LICENSE file, so I was wondering under what license is this project released? I see the related projects being released under MIT, so is this under MIT as well?

Domain config option

For the session cookie, is there a way to specify which domain it belongs to? If not, then I think it could be a good addition.

Session.config do |c|
  c.domain = ENV["KEMAL_ENV"] == "development" ? ".lvh.me" : ".mycooldomain.com"
end

Destroy Session

I can't figure out how to destroy a session. In other words, when a user logs out, I want to create a brand new session when/if they log back in. How do I do this?

Instantiation of New Engines

I'm trying to write a redis storage engine and I'm a bit confused by the initialization of the engine in kemal-session.

When configuring a new engine in kemal, you need to initialize it like the example:

Session.config.engine = Session::FileSystemEngine.new({sessions_dir: "/var/foobar/sessions/"})

Does this mean that every request that is received by kemal creates a new instantiation of the FileSystemEngine?

unable to use session after destroy

Dunno if this is a bug per se, but surprised me. After running an env.session.destroy if I access the session again in that request:

get "/logout" do |env|
  env.session.destroy
  env.session.string("flash", "successfully logged out")
  env.redirect "/login"
end

I get this:

Missing hash key: "9312da9064daec185cc58cb1bf3937f4" (KeyError)
0x82ec567: *Hash(String, String) at /opt/crystal/src/hash.cr 124:9
0x82ec42c: *Hash(String, String) at /opt/crystal/src/hash.cr 61:5
0x83289f8: *Session::MemoryEngine#string:String at /kemal_server/lib/kemal-session/src/kemal-session/engines/memory.cr 136:5
0x83295ef: *Session#string:String at .../kemal-session/src/kemal-session/engine.cr 48:3

(for followers, the work around is to not reuse flash anymore forthat request, it creates a new session on the next request seemingly.

TravisCI Integration

@sdogruyol I think it would be nice to have TravisCI check the code and run specs when reviewing big pull requests like #7 . It would add some additional safety and we can be sure we don't accidentally break something... WDYT?

Missing Hash Key

Well, it looks like this new version of kemal-session doesn't like me. As a workaround to #22, I created a StorableObject and just stuffed something in it (by just having a StorableObject the Union() is no longer empty).

But now I have an issue with session_ids not being found. I followed the trace, looked into the macro cache that the trace pointed to, and found the error is occurring here in the first line of the def string?

Shouldn't this simply be return nil unless @store[session_id]?

def string?(session_id : String, k : String) : String?
  return nil if @store[session_id].nil?
  storage_instance = StorageInstance.from_json(@store[session_id])
  return storage_instance.string?(k)
end

Original error

Missing hash key: "f01e815243295ba4637bff26e4c53d94" (KeyError)
0x564069163713: *Hash(String, String) at /opt/crystal/src/hash.cr 124:9
0x564069163606: *Hash(String, String) at /opt/crystal/src/hash.cr 61:5
0x564069163006: *Session::MemoryEngine#string?<String, String>:(String | Nil) at /home/jason/.cache/crystal/macro122191760.cr 65:25
0x5640691643cd: *Session#string?<String>:(String | Nil) at /home/jason/.cache/crystal/macro121260672.cr 102:9
0x5640690775cf: ~procProc(HTTP::Server::Context, String) at /opt/crystal/src/random.cr 308:3
0x56406915fb26: *Kemal::RouteHandler#process_request<HTTP::Server::Context>:HTTP::Server::Context at /home/jason/crystal/card_game/lib/kemal/src/kemal/route_handler.cr 35:7
0x56406915fa96: *Kemal::RouteHandler#call<HTTP::Server::Context>:HTTP::Server::Context at /home/jason/crystal/card_game/lib/kemal/src/kemal/route_handler.cr 18:7
0x56406917c9d9: *Kemal::WebSocketHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406917c3f4: *Kemal::WebSocketHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/websocket_handler.cr 10:14
0x56406918327a: *Kemal::StaticFileHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406918273e: *Kemal::StaticFileHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/static_file_handler.cr 75:9
0x5640691817d0: *Kemal::CommonExceptionHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x5640691812a0: *Kemal::CommonExceptionHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/common_exception_handler.cr 9:9
0x564069180836: *Kemal::CommonLogHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406917dec1: *Kemal::CommonLogHandler#call<HTTP::Server::Context>:HTTP::Server::Context at /home/jason/crystal/card_game/lib/kemal/src/kemal/common_log_handler.cr 14:35
0x56406915f52b: *Kemal::InitHandler at /opt/crystal/src/http/server/handler.cr 24:7
0x56406915f115: *Kemal::InitHandler#call<HTTP::Server::Context>:(Bool | File::PReader | HTTP::Server::Context | HTTP::Server::Response | HTTP::Server::Response::Output | IO::FileDescriptor+ | Int32 | Nil) at /home/jason/crystal/card_game/lib/kemal/src/kemal/init_handler.cr 11:7
0x5640691c8ff0: *HTTP::Server::RequestProcessor#process<(OpenSSL::SSL::Socket::Server | TCPSocket+), (OpenSSL::SSL::Socket::Server | TCPSocket+), IO::FileDescriptor>:Nil at /opt/crystal/src/http/server/request_processor.cr 39:11
0x5640691c88f9: *HTTP::Server::RequestProcessor#process<(OpenSSL::SSL::Socket::Server | TCPSocket+), (OpenSSL::SSL::Socket::Server | TCPSocket+)>:Nil at /opt/crystal/src/http/server/request_processor.cr 16:3
0x5640691c30b9: *HTTP::Server#handle_client<(TCPSocket+ | Nil)>:Nil at /opt/crystal/src/http/server.cr 174:5
0x564069079033: ~procProc(Nil) at /opt/crystal/src/concurrent.cr 60:3
0x56406908e024: *Fiber#run:(IO::FileDescriptor | Nil) at /opt/crystal/src/fiber.cr 114:3
0x5640690222a6: ~proc2Proc(Fiber, (IO::FileDescriptor | Nil)) at /opt/crystal/src/concurrent.cr 60:3
0x0: ??? at ??

Simplify....

After reading over the code, I can't help thing kemal-session could be significantly simplified by just saving a single object. That is to say, removing all the strings, int32, int64, floats, bools etc.

You could tell users they had to define a storable object. They could then define the structure of the data they want to save. I typically want to save a user, so as long as my user has a to_json method (which is probable if I run an endpoint on it) I can the just nest in as a sub object.

ie

class SessionSchema
    def initialize
    end

    JSON.mapping({
      user:   {type: User, nilable: true},
      basket: {type: Basket, nilable: true},
      flash:{type:String, nilable: true},
    })
    include Session::StorableObject
  end

This would remove many of the current macros and possibly make it easier for developers to add other store engines.

What do you think?

Memory engine not working with objects properly

I've been looking at the implementation of memory engine and I don't think it's correct. Memory Engine should be serializing and unserializing the storable objects as a more true representation of what other engines (file, redis) should be doing. I didn't want to lose track of this, so putting as an issue for now. I don't know if I'll get the time to fix this soon. Could use some help if others do.

Incompatible with Crystal 0.25

When requiring kemal-session, the following exception is raised

require "kemal-session"
^

in lib/kemal-session/src/kemal-session.cr:2: while requiring "./kemal-session/*"

require "./kemal-session/*"
^

in lib/kemal-session/src/kemal-session/engine.cr:90: instantiating 'Kemal::Session::GC:Class#new()'

    GC.new
       ^~~

in lib/kemal-session/src/kemal-session/gc.cr:6: instantiating 'loop()'

          loop do
          ^~~~

in lib/kemal-session/src/kemal-session/gc.cr:6: instantiating 'loop()'

          loop do
          ^~~~

in lib/kemal-session/src/kemal-session/gc.cr:7: instantiating 'Kemal::Session::Engine+#run_gc()'

            Session.config.engine.run_gc
                                  ^~~~~~

in lib/kemal-session/src/kemal-session/engines/file.cr:70: instantiating 'Dir:Class#each_child(String)'

        Dir.each_child(@sessions_dir) do |f|
            ^~~~~~~~~~

in /usr/share/crystal/src/dir.cr:180: instantiating 'Dir:Class#open(String)'

    Dir.open(dirname) do |dir|
        ^~~~

in /usr/share/crystal/src/dir.cr:180: instantiating 'Dir:Class#open(String)'

    Dir.open(dirname) do |dir|
        ^~~~

in /usr/share/crystal/src/dir.cr:181: instantiating 'Dir#each_child()'

      dir.each_child do |filename|
          ^~~~~~~~~~

in /usr/share/crystal/src/dir.cr:181: instantiating 'Dir#each_child()'

      dir.each_child do |filename|
          ^~~~~~~~~~

in lib/kemal-session/src/kemal-session/engines/file.cr:70: instantiating 'Dir:Class#each_child(String)'

        Dir.each_child(@sessions_dir) do |f|
            ^~~~~~~~~~

in lib/kemal-session/src/kemal-session/engines/file.cr:73: undefined method 'stat' for File:Class

            age = Time.utc_now - File.stat(full_path).mtime # mtime is always saved in utc

access session from websocket

Hi

I'm using kemal-session-redis

I'm putting a userId string into the session after a successful login via http post. This works great but when I try to check if the userId is in the session from the websocket - I can see the cookie is present - but the session.strings is empty

 ws "/ws" do |socket, context|
     p context.session.strings #returns {}
     p context.response.cookies #correctly returns the cookie
 end

any idea how I can access the info stored in a session from the websocket?

any help greatly appreciated

alias StorableObjects = Union() Syntax Error

I just did a shards update ('m using the master branch), and an app I'm working on no longer works:

Error in lib/kemal-session/src/kemal-session/storable_object.cr:19: macro didn't expand to a valid program, it expanded to:

================================================================================
--------------------------------------------------------------------------------
   1.       alias StorableObjects = Union()
   2.     
--------------------------------------------------------------------------------
Syntax error in expanded macro: finished:1: expecting token 'CONST', not ')'

      alias StorableObjects = Union()
                                    ^

================================================================================

    macro finished
          ^~~~~~~~

I noticed that #19 was just committed yesterday dealing specifically with StorableObjects.

File session can die if file removed

set the

   config.timeout = Time::Span.new(days: 0, hours: 0, minutes: 10, seconds: 0)

then access your web server so the cookie file is created.

Now wait one minute so the File.utime stuff will be triggered (or possibly until a gc fires that cleans it up?)

Delete the file.

Access your web server.

Error setting time to file './sessions/58c9d216c8ba7334f0bd875798feb61f.json': No such file or directory (Errno)
0x102681975: *CallStack::unwind:Array(Pointer(Void)) at ??
0x102681911: *CallStack#initialize:Array(Pointer(Void)) at ??
0x1026818e8: *CallStack::new:CallStack at ??
0x102656181: *raise:NoReturn at ??
0x1026df2f3: *File::utime<Time, Time, String>:Nil at ??
0x1027b06de: *Session::FileEngine#is_in_cache?:Bool at ??
0x1027b07f9: *Session::FileEngine#string?<String, String>:(String | Nil) at ??
0x1027a0eed: *Session#string?:(String | Nil) at ??
0x1026783fa: ~procProc(HTTP::Server::Context, String)@kemal_server.cr:456 at ??

though it does recover a bit after that.

I'm also not sure how I feel about the file stuff being cached at all, since the cache will persist across requests which means if it's in front of a load balancer, with a shared file system, it wouldn't persist across servers. Maybe this should be noted somewhere? Or I think ideally it would at least re-read "in" the file at the beginning of each request...or at least compare mtime with the last known mtime. Or else be made clear that it doesn't work today with shared storage (or perhaps I'm wrong?)

Cheers!

Not able to run basic usage example

When I run this example, I'm not able to get values back out.

require "kemal"
require "kemal-session"

Kemal::Session.config do |config|
  config.secret = "my_super_secret"
end

get "/set" do |env|
  number = rand(100)
  env.session.int("number", number) # set the value of "number"
  "Random number #{number} set." 
end

get "/get" do |env|
  number  = env.session.int?("number") # get the value of "number"
  "Value of random number is #{number}."
end

Kemal.run

Then I boot it up and hit the endpoints

$ curl localhost:3000/set
Random number 10 set.
$ curl localhost:3000/get
Value of random number is  .

You can see there's no value found when I call get. Also, if I remove the ? from the method call, it throws an exception saying the key "number" isn't there. Maybe I'm missing something?

Accessing cookies

If I try to access cookies from the response before env.session is called, then my cookies are empty.

puts env.response.cookies.inspect   # no cookies
env.session
puts env.response.cookies.inspect   # lots of cookies

Since this is lazy loaded, I think being able to access cookies from env.session like env.session.cookies["my_cookie"] would be helpful. This would allow it to remain lazy loaded.

file session garbage collects files that weren't sessions

for instance if you create a "sessions" directory, then you add a ".git_keep_this_dir" in there so that it will be created on a fresh git clone, eventually, when it garbage collects, it removes the ".git_keep_this_dir" file (along with other stale session files), so it is lost out of version control :|

Int64 Support

I'm using Postgres as a backend and user_id in my app is defined as Int64 (BigInt). That doesn't match up with context.session.int().

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.