GithubHelp home page GithubHelp logo

codax's Introduction

codax

Codax is an idiomatic transactional embedded database for clojure. A codax database functions as a single (potentially enormous) clojure map, with a similar access & modification api.

Clojars Project

Version 1.4.0 implements upgradable transactions. (using with-upgradable-transaction macro) and fixes an RCE vulnerability.

See the changelog for details on upgrading from earlier codax versions.

The Why

Even simple programs frequently benefit from saving data to disk. Unfortunately, there is generally a large semantic leap once you go from values in memory to values on disk. Codax aims to close that semantic gap. While it won't win any speed contests, it is designed to be performant enough for applications serving thousands of users. Most importantly, it is designed to make data persistance as low friction as possible. It is also designed to be effortless to get started with. There are no external libraries to install, and the underlying B+ tree is written directly in clojure.

Fundamentally, I wrote this library for myself in an ongoing effort to make my own life simpler and my own programming more fun. I wanted to share it with the community in the hopes that others may find it does the same for them.

ACID Compliance

Codax provides the following guarantees:

  • Atomic - Every transaction is completed fully, or not at all
  • Consistent - The database always represents a valid clojure map
  • Isolated - No reader or writer will ever see data from an incomplete transaction
  • Durable - All writes are synced to disk before returning

Production Ready?

This library has been, and continues to be, successfully used in production environments.

If your project may eventually outgrow codax, keep an eye on Dresser, which can use codax under the hood and allows you to switch database backends later.

Usage

Basic API

Note: all public api symbols have doc-strings which may provide additional details.

Database Functions

  • open-database! - Opens or creates a database, or returns an existing database connection if it's already open
  • close-database! - Safely closes an open database
  • close-all-databases! - Safely closes all open databases
  • is-open? - Checks if a database is open
  • destroy-database! - Deletes a database and all its data irretrievably (intended for use in tests).

Transaction Macros

These take a database argument and a transaction-symbol and bind the symbol to a newly created transaction. Transactions are isolated from each other. Read-transactions evaluate to the value of their body, but a (successful) write-transaction evaluates to nil.

  • with-read-transaction - creates a read transaction
  • with-write-transaction - creates a write transaction (body must evaluate to a transaction or an exception will be thrown)
  • with-upgradable-transaction - creates a read transaction that will upgrade to a write transaction if the transactions calls any modification function. Details and examples in upgradable transactions.md. added in 1.4.0

In-Transaction Functions

These are all similar to the clojure.core map *-in (e.g. assoc-in) with the following exceptions:

  • their first argument is a transaction instead of a map
  • their second argument is a path (see below)
  • their value argument or result (in the case of update) must be conformant

These must be called within a with-write-transaction or a with-read-transaction expression. Changes will only be persistent if with-write-transaction is used.

  • get-at
  • assoc-at
  • update-at
  • merge-at
  • dissoc-at

Shortcut Functions

These are the same as the transactional-functions except that their first argument is a database instead of a transaction. These are convenience functions which automatically create and execute transactions. The write variants will also return the result of the modification.

  • get-at!
  • assoc-at!
  • update-at!
  • merge-at!
  • dissoc-at!

Seek Functions added in 1.2.0

These allow you to get ordered subsets of data from the database "map" in the form of ordered key-value pairs. Each accepts optional :limit and :reverse keyword parameters. They follow the same naming conventions of the other functions (plain variants expect a tx argument and ! variants expect a db argument).

  • seek-at & seek-at! - get ordered key-value pairs from the map at the provided path
  • seek-from & seek-from! - get ordered key-value pairs from the map at the provided path for keys >= start-val
  • seek-to & seek-to! - get ordered key-value pairs from the map at the provided path for keys <= end-val
  • seek-range & seek-range! - get ordered key-value pairs from the map at the provided path for keys >= start-val and <= end-val
  • seek-prefix & seek-prefix! - get ordered key-value pairs from the map at the provided path for string or keyword keys which begin with val-prefix
  • seek-prefix-range & seek-prefix-range! - get ordered key-value pairs from the map at the provided path for string or keyword keys which begin with a value between (inclusive) start-prefix & end-prefix

See Seek Examples

Paths

A path is a vector of keys similar to the [k & ks] used in function like assoc-in with a few exceptions:

  • they are limited to the following types:
    • Symbols
    • Keywords
    • Strings
    • Numbers (float/double use is strongly discouraged)
    • true
    • false
    • nil
    • java.time.Instant
    • org.joda.time.DateTime
  • the path can only target nested maps, and cannot be used to descend into other data structures (e.g. vectors).
  • you can get the empty path (e.g. (get-at db []) returns the full database) but you cannot modify it (e.g. (assoc-at [] :foo) throws an error)

If you need support for additional types, please review doc/types.md.

Conformant Values

  • non-map values of any type serializable by nippy
    • this will only be relevant to you if you are storing custom records or exotic datatypes. Out of the box, virtually all standard clojure datatypes are supported (i.e. you don't need to do anything special to store lists/vectors/sets/etc.)
    • the serialization is performed automatically, you do not need to serialize values manually
  • maps and nested maps whose keys conform to the valid path types listed above
  • custom types may require whitelisting, see #30

Transactions

Immutability

Transactions are immutable. Each transformation (e.g. assoc-at, update-at) returns a new transaction, it does not modify the transaction. Essentially you should treat them as you would a standard clojure map, one that you interact with using the *-at functions.

Example:

(c/with-write-transaction [db tx-original]
  (let [tx-a (c/assoc-at tx-original [:letter] "a")
        tx-b (c/assoc-at tx-original [:letter] "b")]
    tx-a))

(c/get-at! db [:letter]) ; "a"

See the FAQ for examples of potential pitfalls.

Visibility

Changes in a transaction are only visible to subsequent transformations on that transaction. They are not visible anywhere else until committed (by being the final result in the body of a with-write-transaction expression). The changes are also not visible in any read transaction opened before the write transaction is committed.

Example:

(c/with-write-transaction [db tx]
  (-> tx
      (c/assoc-at [:number] 1000)
      (c/update-at [:number] inc)))

(c/get-at! db [:number]) ; 1001

Exceptions

If an Exception is thrown within a with-write-transaction expression, the transaction is aborted and no changes are persisted.

Locking

Write transactions block other write transactions (though they do not block read transactions). It is best to avoid doing any computationally complex or IO heavy tasks (such as fetching remote data) inside a with-write-transaction block. See Performance for more details.

Examples

(require [codax.core :as c])

Simple Use

(def db (c/open-database! "data/demo-database")) ;

(c/assoc-at! db [:assets :people] {0 {:name "Alice"
                                      :occupation "Programmer"
                                      :age 42}
                                   1 {:name "Bob"
                                      :occupation "Writer"
                                      :age 27}}) ; {0 {:age 42, :name "Alice", ...}, 1 {:age 27, :name "Bob", ...}}

(c/get-at! db [:assets :people 0]) ; {:name "Alice" :occupation "Programmer" :age 42}

(c/update-at! db [:assets :people 1 :age] inc) ; 28

(c/merge-at! db [:assets] {:tools {"hammer" true
                                   "keyboard" true}}) ; {:people {...} :tools {"hammer" true, "keyboard" true}}

(c/get-at! db [:assets])
;;  {:people {0 {:name "Alice"
;;               :occupation "Programmer"
;;               :age 42}
;;            1 {:name "Bob"
;;               :occupation "Writer"
;;               :age 28}}
;;   :tools {"hammer" true
;;           "keyboard" true}}

(c/close-database! db)

Transaction Example

(def db (c/open-database! "data/demo-database"))

;;;; init
(c/with-write-transaction [db tx]
  (c/assoc-at tx [:counters] {:id 0 :users 0}))

;;;; user fns
(defn add-user
  "create a user and assign them an id"
  [username]
  (c/with-write-transaction [db tx]
    (when (c/get-at tx [:usernames username] )
      (throw (Exception. "username already exists")))
    (let [user-id (c/get-at tx [:counters :id])
          user {:id user-id
                :username username
                :timestamp (System/currentTimeMillis)}]
      (-> tx
          (c/assoc-at [:users user-id] user)
          (c/assoc-at [:usernames username] user-id)
          (c/update-at [:counters :id] inc)
          (c/update-at [:counters :users] inc)))))

(defn get-user
  "fetch a user by their username"
  [username]
  (c/with-read-transaction [db tx]
    (when-let [user-id (c/get-at tx [:usernames username])]
      (c/get-at tx [:users user-id]))))

(defn rename-user
  "change a username"
  [username new-username]
  (c/with-write-transaction [db tx]
    (when (c/get-at tx [:usernames new-username] )
      (throw (Exception. "username already exists")))
    (when-let [user-id (c/get-at tx [:usernames username])]
      (-> tx
          (c/dissoc-at [:usernames username])
          (c/assoc-at [:usernames new-username] user-id)
          (c/assoc-at [:users user-id :username] new-username)))))

(defn remove-user
  "remove a user"
  [username]
  (c/with-write-transaction [db tx]
    (when-let [user-id (c/get-at tx [:usernames username])]
      (-> tx
          (c/dissoc-at [:username username])
          (c/dissoc-at [:users user-id])
          (c/update-at [:counters :users] dec)))))


;;;;; edit users

(c/get-at! db) ; {:counters {:id 0, :users 0}}


(add-user "charlie") ; nil
(c/get-at! db)
;;  {:counters {:id 1, :users 1},
;;   :usernames {"charlie" 0},
;;   :users {0 {:id 0, :timestamp 1484529469567, :username "charlie"}}}


(add-user "diane") ; nil
(c/get-at! db)
;;  {:counters {:id 2, :users 2},
;;   :usernames {"charlie" 0, "diane" 1},
;;   :users
;;   {0 {:id 0, :timestamp 1484529469567, :username "charlie"},
;;    1 {:id 1, :timestamp 1484529603444, :username "diane"}}}


(rename-user "charlie" "chuck") ; nil
(c/get-at! db)
;;  {:counters {:id 2, :users 2},
;;   :usernames {"chuck" 0, "diane" 1},
;;   :users
;;   {0 {:id 0, :timestamp 1484529469567, :username "chuck"},
;;    1 {:id 1, :timestamp 1484529603444, :username "diane"}}}


(remove-user "diane") ; nil
(c/get-at! db)
;;  {:counters {:id 2, :users 1},
;;   :usernames {"chuck" 0, "diane" 1},
;;   :users {0 {:id 0, :timestamp 1484529469567, :username "chuck"}}}



(c/close-database! db)

Seek Examples

Directory Example

(def db (c/open-database! "data/example-database"))

(c/assoc-at! db [:directory]
             {"Alice" {:ext 247, :dept "qa"}
              "Barbara" {:ext 228, :dept "qa"}
              "Damian" {:ext 476, :dept "hr"}
              "Adam" {:ext 357, :dept "hr"}
              "Frank" {:ext 113, :dept "hr"}
              "Bill" {:ext 234, :dept "sales"}
              "Evelyn" {:ext 337, :dept "dev"}
              "Chuck" {:ext 482, :dept "sales"}
              "Emily" {:ext 435, :dept "dev"}
              "Diane" {:ext 245, :dept "dev"}
              "Chelsea" {:ext 345, :dept "qa"}
              "Bob" {:ext 326, :dept "sales"}})

;; - seek-at -

(c/seek-at! db [:directory])
;;[["Adam" {:dept "hr", :ext 357}]
;; ["Alice" {:dept "qa", :ext 247}]
;; ["Barbara" {:dept "qa", :ext 228}]
;; ["Bill" {:dept "sales", :ext 234}]
;; ["Bob" {:dept "sales", :ext 326}]
;; ["Chelsea" {:dept "qa", :ext 345}]
;; ["Chuck" {:dept "sales", :ext 482}]
;; ["Damian" {:dept "hr", :ext 476}]
;; ["Diane" {:dept "dev", :ext 245}]
;; ["Emily" {:dept "dev", :ext 435}]
;; ["Evelyn" {:dept "dev", :ext 337}]
;; ["Frank" {:dept "hr", :ext 113}]]

(c/seek-at! db [:directory] :limit 3)
;;[["Adam" {:dept "hr", :ext 357}]
;; ["Alice" {:dept "qa", :ext 247}]
;; ["Barbara" {:dept "qa", :ext 228}]]

(c/seek-at! db [:directory] :limit 3 :reverse true)
;;[["Frank" {:ext 113, :dept "hr"}]
;; ["Evelyn" {:ext 337, :dept "dev"}]
;; ["Emily" {:ext 435, :dept "dev"}]]


;; - seek-prefix -

(c/seek-prefix! db [:directory] "B")
;;[["Barbara" {:dept "qa", :ext 228}]
;; ["Bill" {:dept "sales", :ext 234}]
;; ["Bob" {:dept "sales", :ext 326}]]


;; - seek-prefix-range -

(c/seek-prefix-range! db [:directory] "B" "D")
;;[["Barbara" {:dept "qa", :ext 228}]
;; ["Bill" {:dept "sales", :ext 234}]
;; ["Bob" {:dept "sales", :ext 326}]
;; ["Chelsea" {:dept "qa", :ext 345}]
;; ["Chuck" {:dept "sales", :ext 482}]
;; ["Damian" {:dept "hr", :ext 476}]
;; ["Diane" {:dept "dev", :ext 245}]]


(c/close-database! db)

Messaging Example

(def db (c/open-database! "data/example-database")) ;

(defn post-message!
  ([user body]
   (post-message! (java.time.Instant/now) user body))
  ([inst user body]
   (c/assoc-at! db [:messages inst] {:user user
                                     :body body})))

(defn process-messages
  [messages]
  (map (fn [[inst m]] (assoc m :time (str inst))) messages))


(defn get-messages-before [ts]
  (process-messages
   (c/seek-to! db [:messages] (.toInstant ts))))

(defn get-messages-after [ts]
  (process-messages
   (c/seek-from! db [:messages] (.toInstant ts))))

(defn get-messages-between [start-ts end-ts]
  (process-messages
   (c/seek-range! db [:messages] (.toInstant start-ts) (.toInstant end-ts))))

(defn get-recent-messages [n]
  (-> (c/seek-at! db [:messages] :limit n :reverse true)
      process-messages ;;
      reverse)); we reverse the result because we want the messages to be in chronological order
               ; but we needed to use the :reverse seek parameter to prevent collecting all
               ; of the messages from the beginning of time (there could be many thousands!)



(defn simulate-message! [date-time user body]
  (post-message! (.toInstant date-time) user body))

(simulate-message! #inst "2020-06-06T11:01" "Bobby" "Hello")
(simulate-message! #inst "2020-06-06T11:02" "Alice" "Welcome, Bobby")
(simulate-message! #inst "2020-06-06T11:03" "Bobby" "I was wondering how codax seeking works?")
(simulate-message! #inst "2020-06-06T11:07" "Alice" "Please be more specific, have you read the docs/examples?")
(simulate-message! #inst "2020-06-06T11:08" "Bobby" "Oh, I guess I should do that.")

(simulate-message! #inst "2020-06-07T14:30" "Chuck" "Anybody here?")
(simulate-message! #inst "2020-06-07T14:35" "Chuck" "Guess not...")

(simulate-message! #inst "2020-06-08T16:50" "Bobby" "Okay, so I read the docs. What is the :reverse param for?")
(simulate-message! #inst "2020-06-08T16:55" "Alice" "Basically, it seeks from the end and works backwards")
(simulate-message! #inst "2020-06-08T16:56" "Bobby" "Why would I do that?")
(simulate-message! #inst "2020-06-08T16:57" "Alice" "Well, generally it is used to grab just the end of a long dataset.")


(get-recent-messages 3)
;;({:user "Alice" :time "2020-06-08T16:55:00Z" :body "Basically, it seeks from the end and works backwards"}
;; {:user "Bobby" :time "2020-06-08T16:56:00Z" :body "Why would I do that?"}
;; {:user "Alice" :time "2020-06-08T16:57:00Z" :body "Well, generally it is used to grab just the end of a long dataset." })

(get-messages-after #inst "2020-06-07T14:32")
;;({:user "Chuck" :time "2020-06-07T14:35:00Z" :body "Guess not..."}
;; {:user "Bobby" :time "2020-06-08T16:50:00Z" :body "Okay, so I read the docs. What is the :reverse param for?"}
;; {:user "Alice" :time "2020-06-08T16:55:00Z" :body "Basically, it seeks from the end and works backwards"}
;; {:user "Bobby" :time "2020-06-08T16:56:00Z" :body "Why would I do that?"}
;; {:user "Alice" :time "2020-06-08T16:57:00Z" :body "Well, generally it is used to grab just the end of a long dataset." })

(get-messages-before #inst "2020-06-06T11:05")
;;({:user "Bobby" :time "2020-06-06T11:01:00Z" :body "Hello"}
;; {:user "Alice" :time "2020-06-06T11:02:00Z" :body "Welcome, Bobby"}
;; {:user "Bobby" :time "2020-06-06T11:03:00Z" :body "I was wondering how codax seeking works?"})


(get-messages-between #inst "2020-06-07"
                      #inst "2020-06-07T23:59")
;;({:user "Chuck" :time "2020-06-07T14:30:00Z" :body "Anybody here?"}
;; {:user "Chuck" :time "2020-06-07T14:35:00Z" :body "Guess not..."})

(c/close-database! db)

Frequently Asked Questions

Why aren't all my changes being saved?

Because transactions are immutable, if an updated transaction is discarded, the transformations it contains will not be committed.

Incorrect:

(c/with-write-transaction [db tx]
  (c/assoc-at tx [:users 1] "Alice") ; this write is "lost"
  (c/assoc-at tx [:users 2] "Bob"))

(c/get-at! db [:users]) ; {2 "Bob"}

Correct:

(c/with-write-transaction [db tx]
  (-> tx ; thread the transaction through multiple transformations
      (c/assoc-at [:users 1] "Alice")
      (c/assoc-at [:users 2] "Bob")))

(c/get-at! db [:users]) ; {1 "Alice" 2 "Bob"}

Why am I getting a NullPointerException in my Write Transaction?

A common cause is that the body of the with-write-transaction form is not evaluating to (returning) a transaction.

Incorrect:

(defn init-counter! []
  (c/with-write-transaction [db tx]
    (when-not (c/get-at tx [:counter])
      (c/assoc-at tx [:counter] 0))))

(init-counter!) ; nil (it works the first time)
(init-counter!) ; java.lang.NullPointerException (the body evaluates to nil)

Correct:

(defn init-counter! []
  (c/with-write-transaction [db tx]
    (if-not (c/get-at tx [:counter])
      (c/assoc-at tx [:counter] 0)
      tx))) ;; if the counter is already initialized, return the unmodified transaction

(init-counter!) ; nil
(init-counter!) ; nil

Performance

Codax is geared towards read-heavy workloads.

  • Read-Transactions block nothing
  • Write-Transactions block other Write-Transactions
  • Stage-1 Compaction blocks Write-Transactions (slow)
  • Stage-2 Compaction blocks both Reader-Transactions and Write-Transactions (fast)

Benchmark Results

Jan 14, 2017

The following figures are for a database populated with 16,000,000 (map-leaf) values running on a Digital Ocean 2-core 2GB RAM instance. The write transactions have an average "path" length of 6 and an average 7 leaf values.

  • ~320 write-transaction/second
  • ~1640 read-transactions/second
  • ~2700ms per compaction (compaction happens automatically every 10,000 writes)

This benchmark is a bit dated, but a similar benchmarking function is available as codax.bench.performance/run-benchmark.

Bugs

...

Contributing

Insights, suggestions, and PRs are very welcome.

License

Copyright ยฉ 2023 David Scarpetti

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

codax's People

Contributors

abhi18av avatar dscarpetti avatar frozenlock avatar jessesherlock 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

codax's Issues

Increasing file size

I've noticed that the codax file is increasing in size even when the stored map is small.
It seems related to the number of manipulations.

A small map (~100kb when in a text file) will give a nodes file bigger than 10Mb after a few dozens of overwrites.

Error requiring codax

I created an app with lein new luminus myapp, added codax to project.clj and required it in src/clj/myapp/routes/home.clj.

After running lein run I get the following error:
Exception in thread "main" java.lang.ExceptionInInitializerError at clojure.main.<clinit>(main.java:20) Caused by: java.lang.IllegalArgumentException: No matching method: repl, compiling:(codax/core.clj:161:3) at clojure.lang.Compiler.analyzeSeq(Compiler.java:6875) at clojure.lang.Compiler.analyze(Compiler.java:6669) at clojure.lang.Compiler.analyzeSeq(Compiler.java:6856) at clojure.lang.Compiler.analyze(Compiler.java:6669) at clojure.lang.Compiler.analyze(Compiler.java:6625) at clojure.lang.Compiler$BodyExpr$Parser.parse(Compiler.java:6001) at clojure.lang.Compiler$FnMethod.parse(Compiler.java:5380) at clojure.lang.Compiler$FnExpr.parse(Compiler.java:3972) at clojure.lang.Compiler.analyzeSeq(Compiler.java:6866) at clojure.lang.Compiler.analyze(Compiler.java:6669) at clojure.lang.Compiler.analyzeSeq(Compiler.java:6856) at clojure.lang.Compiler.analyze(Compiler.java:6669) at clojure.lang.Compiler.access$300(Compiler.java:38) at clojure.lang.Compiler$DefExpr$Parser.parse(Compiler.java:589) at clojure.lang.Compiler.analyzeSeq(Compiler.java:6868) at clojure.lang.Compiler.analyze(Compiler.java:6669) at clojure.lang.Compiler.analyze(Compiler.java:6625) at clojure.lang.Compiler.eval(Compiler.java:6931) at clojure.lang.Compiler.load(Compiler.java:7379) at clojure.lang.RT.loadResourceScript(RT.java:372) at clojure.lang.RT.loadResourceScript(RT.java:363) at clojure.lang.RT.load(RT.java:453) at clojure.lang.RT.load(RT.java:419) at clojure.core$load$fn__5677.invoke(core.clj:5893) at clojure.core$load.invokeStatic(core.clj:5892) at clojure.core$load.doInvoke(core.clj:5876) at clojure.lang.RestFn.invoke(RestFn.java:408) at clojure.core$load_one.invokeStatic(core.clj:5697) at clojure.core$load_one.invoke(core.clj:5692) at clojure.core$load_lib$fn__5626.invoke(core.clj:5737) at clojure.core$load_lib.invokeStatic(core.clj:5736) at clojure.core$load_lib.doInvoke(core.clj:5717) at clojure.lang.RestFn.applyTo(RestFn.java:142) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$load_libs.invokeStatic(core.clj:5774) at clojure.core$load_libs.doInvoke(core.clj:5758) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$require.invokeStatic(core.clj:5796) at clojure.core$require.doInvoke(core.clj:5796) at clojure.lang.RestFn.invoke(RestFn.java:482) at myapp.routes.home$eval5414$loading__5569__auto____5415.invoke(home.clj:1) at myapp.routes.home$eval5414.invokeStatic(home.clj:1) at myapp.routes.home$eval5414.invoke(home.clj:1) at clojure.lang.Compiler.eval(Compiler.java:6927) at clojure.lang.Compiler.eval(Compiler.java:6916) at clojure.lang.Compiler.load(Compiler.java:7379) at clojure.lang.RT.loadResourceScript(RT.java:372) at clojure.lang.RT.loadResourceScript(RT.java:363) at clojure.lang.RT.load(RT.java:453) at clojure.lang.RT.load(RT.java:419) at clojure.core$load$fn__5677.invoke(core.clj:5893) at clojure.core$load.invokeStatic(core.clj:5892) at clojure.core$load.doInvoke(core.clj:5876) at clojure.lang.RestFn.invoke(RestFn.java:408) at clojure.core$load_one.invokeStatic(core.clj:5697) at clojure.core$load_one.invoke(core.clj:5692) at clojure.core$load_lib$fn__5626.invoke(core.clj:5737) at clojure.core$load_lib.invokeStatic(core.clj:5736) at clojure.core$load_lib.doInvoke(core.clj:5717) at clojure.lang.RestFn.applyTo(RestFn.java:142) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$load_libs.invokeStatic(core.clj:5774) at clojure.core$load_libs.doInvoke(core.clj:5758) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$require.invokeStatic(core.clj:5796) at clojure.core$require.doInvoke(core.clj:5796) at clojure.lang.RestFn.invoke(RestFn.java:551) at myapp.handler$eval288$loading__5569__auto____289.invoke(handler.clj:1) at myapp.handler$eval288.invokeStatic(handler.clj:1) at myapp.handler$eval288.invoke(handler.clj:1) at clojure.lang.Compiler.eval(Compiler.java:6927) at clojure.lang.Compiler.eval(Compiler.java:6916) at clojure.lang.Compiler.load(Compiler.java:7379) at clojure.lang.RT.loadResourceScript(RT.java:372) at clojure.lang.RT.loadResourceScript(RT.java:363) at clojure.lang.RT.load(RT.java:453) at clojure.lang.RT.load(RT.java:419) at clojure.core$load$fn__5677.invoke(core.clj:5893) at clojure.core$load.invokeStatic(core.clj:5892) at clojure.core$load.doInvoke(core.clj:5876) at clojure.lang.RestFn.invoke(RestFn.java:408) at clojure.core$load_one.invokeStatic(core.clj:5697) at clojure.core$load_one.invoke(core.clj:5692) at clojure.core$load_lib$fn__5626.invoke(core.clj:5737) at clojure.core$load_lib.invokeStatic(core.clj:5736) at clojure.core$load_lib.doInvoke(core.clj:5717) at clojure.lang.RestFn.applyTo(RestFn.java:142) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$load_libs.invokeStatic(core.clj:5774) at clojure.core$load_libs.doInvoke(core.clj:5758) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$require.invokeStatic(core.clj:5796) at clojure.core$require.doInvoke(core.clj:5796) at clojure.lang.RestFn.invoke(RestFn.java:551) at myapp.core$eval282$loading__5569__auto____283.invoke(core.clj:1) at myapp.core$eval282.invokeStatic(core.clj:1) at myapp.core$eval282.invoke(core.clj:1) at clojure.lang.Compiler.eval(Compiler.java:6927) at clojure.lang.Compiler.eval(Compiler.java:6916) at clojure.lang.Compiler.load(Compiler.java:7379) at clojure.lang.RT.loadResourceScript(RT.java:372) at clojure.lang.RT.loadResourceScript(RT.java:363) at clojure.lang.RT.load(RT.java:453) at clojure.lang.RT.load(RT.java:419) at clojure.core$load$fn__5677.invoke(core.clj:5893) at clojure.core$load.invokeStatic(core.clj:5892) at clojure.core$load.doInvoke(core.clj:5876) at clojure.lang.RestFn.invoke(RestFn.java:408) at clojure.core$load_one.invokeStatic(core.clj:5697) at clojure.core$load_one.invoke(core.clj:5692) at clojure.core$load_lib$fn__5626.invoke(core.clj:5737) at clojure.core$load_lib.invokeStatic(core.clj:5736) at clojure.core$load_lib.doInvoke(core.clj:5717) at clojure.lang.RestFn.applyTo(RestFn.java:142) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$load_libs.invokeStatic(core.clj:5774) at clojure.core$load_libs.doInvoke(core.clj:5758) at clojure.lang.RestFn.applyTo(RestFn.java:137) at clojure.core$apply.invokeStatic(core.clj:648) at clojure.core$require.invokeStatic(core.clj:5796) at clojure.core$require.doInvoke(core.clj:5796) at clojure.lang.RestFn.invoke(RestFn.java:421) at user$eval3$loading__5569__auto____4.invoke(user.clj:1) at user$eval3.invokeStatic(user.clj:1) at user$eval3.invoke(user.clj:1) at clojure.lang.Compiler.eval(Compiler.java:6927) at clojure.lang.Compiler.eval(Compiler.java:6916) at clojure.lang.Compiler.load(Compiler.java:7379) at clojure.lang.RT.loadResourceScript(RT.java:372) at clojure.lang.RT.loadResourceScript(RT.java:359) at clojure.lang.RT.maybeLoadResourceScript(RT.java:355) at clojure.lang.RT.doInit(RT.java:475) at clojure.lang.RT.<clinit>(RT.java:331) ... 1 more Caused by: java.lang.IllegalArgumentException: No matching method: repl at clojure.lang.Compiler$StaticMethodExpr.<init>(Compiler.java:1642) at clojure.lang.Compiler$HostExpr$Parser.parse(Compiler.java:1011) at clojure.lang.Compiler.analyzeSeq(Compiler.java:6868) ... 136 more

I can comment out the require in home.clj and the project runs fine, uncomment it and everything still works. Any idea why this happens?

let did not conform to spec

after upgraded to [codax "1.0.1-SNAPSHOT"]

clojure.lang.Compiler$CompilerException: clojure.lang.ExceptionInfo: Call to clojure.core/let did not conform to spec:
In: [0 0] val: (if (get no-graphs (first path)) (get-in)) fails spec: :clojure.core.specs.alpha/local-name at: [:args :bindings :binding :sym] predicate: simple-symbol?
In: [0 0] val: (if (get no-graphs (first path)) (get-in)) fails spec: :clojure.core.specs.alpha/seq-binding-form at: [:args :bindings :binding :seq] predicate: vector?
In: [0 0 0] val: if fails spec: :clojure.core.specs.alpha/map-binding at: [:args :bindings :binding :map :mb] predicate: vector?
In: [0 0 0] val: if fails spec: :clojure.core.specs.alpha/ns-keys at: [:args :bindings :binding :map :nsk] predicate: vector?
In: [0 0 0] val: if fails spec: :clojure.core.specs.alpha/map-bindings at: [:args :bindings :binding :map :msb] predicate: vector?
In: [0 0 1] val: (get no-graphs (first path)) fails spec: :clojure.core.specs.alpha/map-binding at: [:args :bindings :binding :map :mb] predicate: vector?
In: [0 0 1] val: (get no-graphs (first path)) fails spec: :clojure.core.specs.alpha/ns-keys at: [:args :bindings :binding :map :nsk] predicate: vector?
In: [0 0 1] val: (get no-graphs (first path)) fails spec: :clojure.core.specs.alpha/map-bindings at: [:args :bindings :binding :map :msb] predicate: vector?
In: [0 0 2] val: (get-in) fails spec: :clojure.core.specs.alpha/map-binding at: [:args :bindings :binding :map :mb] predicate: vector?
In: [0 0 2] val: (get-in) fails spec: :clojure.core.specs.alpha/ns-keys at: [:args :bindings :binding :map :nsk] predicate: vector?
In: [0 0 2] val: (get-in) fails spec: :clojure.core.specs.alpha/map-bindings at: [:args :bindings :binding :map :msb] predicate: vector?
In: [0 0] val: (if (get no-graphs (first path)) (get-in)) fails spec: :clojure.core.specs.alpha/map-special-binding at: [:args :bindings :binding :map] predicate: map?
:clojure.spec.alpha/args ([(if (get no-graphs (first path)) (get-in))])
{:clojure.spec.alpha/problems ({:path [:args :bindings :binding :sym], :pred simple-symbol?, :val (if (get no-graphs (first path)) (get-in)), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/local-name], :in [0 0]} {:path [:args :bindings :binding :seq], :pred vector?, :val (if (get no-graphs (first path)) (get-in)), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/seq-binding-form], :in [0 0]} {:path [:args :bindings :binding :map :mb], :pred vector?, :val if, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding], :in [0 0 0]} {:path [:args :bindings :binding :map :nsk], :pred vector?, :val if, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 0]} {:path [:args :bindings :binding :map :msb], :pred vector?, :val if, :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 0]} {:path [:args :bindings :binding :map :mb], :pred vector?, :val (get no-graphs (first path)), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding], :in [0 0 1]} {:path [:args :bindings :binding :map :nsk], :pred vector?, :val (get no-graphs (first path)), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 1]} {:path [:args :bindings :binding :map :msb], :pred vector?, :val (get no-graphs (first path)), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 1]} {:path [:args :bindings :binding :map :mb], :pred vector?, :val (get-in), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/map-binding], :in [0 0 2]} {:path [:args :bindings :binding :map :nsk], :pred vector?, :val (get-in), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings :clojure.core.specs.alpha/ns-keys], :in [0 0 2]} {:path [:args :bindings :binding :map :msb], :pred vector?, :val (get-in), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-bindings], :in [0 0 2]} {:path [:args :bindings :binding :map], :pred map?, :val (if (get no-graphs (first path)) (get-in)), :via [:clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/bindings :clojure.core.specs.alpha/binding :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/binding-form :clojure.core.specs.alpha/map-binding-form :clojure.core.specs.alpha/map-special-binding], :in [0 0]}), :clojure.spec.alpha/args ([(if (get no-graphs (first path)) (get-in))])}, compiling:(viri/db.clj:157:18)

Error after :require codax

Any idea how to fix this? I really want to use it. ๐Ÿ˜ฐ
clojure.lang.Compiler$CompilerException: clojure.lang.ExceptionInfo: Call to clojure.core/defn did not conform to spec: In: [0] val: codax.pathwise/encode-element fails spec: :clojure.core.specs.alpha/defn-args at: [:args :name] predicate: simple-symbol? :clojure.spec.alpha/args (codax.pathwise/encode-element [el65854] (clojure.core/cond (true? el65854) (clojure.core/str (clojure.core/char 33) ((fn [_] "") el65854) codax.pathwise/+delim+) (date-time? el65854) (clojure.core/str (clojure.core/char 36) (encode-date-time el65854) codax.pathwise/+delim+) (neg-infinity? el65854) (clojure.core/str (clojure.core/char 48) ((fn [_] "") el65854) codax.pathwise/+delim+) (pos-infinity? el65854) (clojure.core/str (clojure.core/char 50) ((fn [_] "") el65854) codax.pathwise/+delim+) (number? el65854) (clojure.core/str (clojure.core/char 49) (encode-number el65854) codax.pathwise/+delim+) (symbol? el65854) (clojure.core/str (clojure.core/char 104) (encode-symbol el65854) codax.pathwise/+delim+) (false? el65854) (clojure.core/str (clojure.core/char 32) ((fn [_] "") el65854) codax.pathwise/+delim+) (string? el65854) (clojure.core/str (clojure.core/char 112) (encode-string el65854) codax.pathwise/+delim+) ((fn* [p1__65853#] (or (vector? p1__65853#) (list? p1__65853#))) el65854) (clojure.core/str (clojure.core/char 160) (encode-vector el65854) codax.pathwise/+delim+) (keyword? el65854) (clojure.core/str (clojure.core/char 105) (encode-keyword el65854) codax.pathwise/+delim+) (nil? el65854) (clojure.core/str (clojure.core/char 16) ((fn [_] "") el65854) codax.pathwise/+delim+) :else (throw (java.lang.Exception. (clojure.core/str "no method for encoding " el65854 ""))))) {:clojure.spec.alpha/problems [{:path [:args :name], :pred simple-symbol?, :val codax.pathwise/encode-element, :via [:clojure.core.specs.alpha/defn-args :clojure.core.specs.alpha/defn-args], :in [0]}], :clojure.spec.alpha/args (codax.pathwise/encode-element [el65854] (clojure.core/cond (true? el65854) (clojure.core/str (clojure.core/char 33) ((fn [_] "") el65854) codax.pathwise/+delim+) (date-time? el65854) (clojure.core/str (clojure.core/char 36) (encode-date-time el65854) codax.pathwise/+delim+) (neg-infinity? el65854) (clojure.core/str (clojure.core/char 48) ((fn [_] "") el65854) codax.pathwise/+delim+) (pos-infinity? el65854) (clojure.core/str (clojure.core/char 50) ((fn [_] "") el65854) codax.pathwise/+delim+) (number? el65854) (clojure.core/str (clojure.core/char 49) (encode-number el65854) codax.pathwise/+delim+) (symbol? el65854) (clojure.core/str (clojure.core/char 104) (encode-symbol el65854) codax.pathwise/+delim+) (false? el65854) (clojure.core/str (clojure.core/char 32) ((fn [_] "") el65854) codax.pathwise/+delim+) (string? el65854) (clojure.core/str (clojure.core/char 112) (encode-string el65854) codax.pathwise/+delim+) ((fn* [p1__65853#] (or (vector? p1__65853#) (list? p1__65853#))) el65854) (clojure.core/str (clojure.core/char 160) (encode-vector el65854) codax.pathwise/+delim+) (keyword? el65854) (clojure.core/str (clojure.core/char 105) (encode-keyword el65854) codax.pathwise/+delim+) (nil? el65854) (clojure.core/str (clojure.core/char 16) ((fn [_] "") el65854) codax.pathwise/+delim+) :else (throw (java.lang.Exception. (clojure.core/str "no method for encoding " el65854 "")))))}, compiling:(codax/pathwise.clj:108:1)

Failing tests - Timezone related?

The tests relying on parsing dates (Ex: "June 6, 2020 11:01") fail because of a 2 hours difference between the expected and actual values.

  • codax.example-test/messaging-example
  • codax.example-test/bulk

I suspect this is because the parsed time is assumed to be in the local timezone, but the expected value is in UTC.

A possible alternative would be to leverage the #inst tag.
Seconds, milliseconds and the timezone can be omitted to reduce the visual noise:

#inst "2023-08-12T01:00"
; => #inst "2023-08-12T01:00:00.000-00:00"

The conversion to Instant is also pretty straightforward:

(.toInstant #inst "2023-08-12T01:00")
;=> #object[java.time.Instant 0x60208ef "2023-08-12T01:00:00Z"]

Before:

(simulate-message! "June 6, 2020 11:01" "Bobby" "Hello")

After:

(simulate-message! #inst "2020-06-06T11:01" "Bobby" "Hello")

Non-breaking space in a map's keyword string

I'm a big fan of this library, many thanks!

I just stumbled over the following small issue: a "non-breaking space" in a string-key of a map produces

Execution error (NullPointerException) at codax.pathwise/eval9604$fn$fn$fn$fn (pathwise.clj:136).
Cannot invoke "Object.toString()" because "s" is null

with-write-transaction

I'm doing like this:

(defn init-db [ db ]
(c/with-write-transaction [db tx]
(when-not (c/get-at tx [:counters :user-id])
(c/assoc-at tx [:counters :user-id] 1))))

This is for the one-time DB initialization. As you see, if the DB was already initialized, the body of the when-not will not be evaluated, nothing is written to DB. This leads to NullPointerException clojure.lang.RT.longCast (RT.java:1259)

Add examples of nippy

Hi @dscarpetti

First off, thanks for this wonderful library!

My request is that could you please add some nippy exames in the ReadMe ? I'm a beginner in clojure so I'd love to see a couple examples of non-map data being stored via nippy.

Error while 2 sequential writes in 1 transaction

I have following function

(defn create-user! [ email password db]
  (c/with-write-transaction [db tx]
    (let [encrypted-password (p/encrypt password)
          next-user-id (inc (c/get-at tx [:counters :user-id]))
          user {:email email
                :password-hash encrypted-password}]
      (c/merge-at tx [:users] {next-user-id user})
      (c/assoc-at tx [:counters :user-id] next-user-id))))

As you see I have one read and two writes within one transaction. But only the last write occurs. I expect that both writes should occur.

AOT causes problem with clojurescript

When using 1.2.0-snapshot and running figwheel, I get this error :

  No reader function for tag Inf

  986  (js-mod (Math/floor o) 2147483647)
  987  (case o
  988    ##Inf
              ^--- No reader function for tag Inf
  989    2146435072
  990    ##-Inf
  991    -1048576

However, re-adding ^:skip-aot solve this.

Add support for maps as keys (pathwise)

Clojurians are used to be able to use pretty much anything as keys, including maps.

Codax is almost there, but lacks support for maps.

I came up with a simple encoder/decoder that leverages Nippy:

(ns my-ns
  (:require [codax.core :as c]
            [taoensso.nippy :as nippy])
  (:import (java.util Base64)))

;; Simply encoding bytes to strings (String. bytes) causes the data to
;; be corrupted after a round trip in Codax.  Encoding in b64 solves
;; this.

(defn bytes-to-base64 [bytes]
  (let [encoder (Base64/getEncoder)]
    (.encodeToString encoder bytes)))

(defn base64-to-bytes [base64-string]
  (let [decoder (Base64/getDecoder)]
    (.decode decoder base64-string)))

(c/defpathtype [0x71
                clojure.lang.PersistentHashMap
                clojure.lang.PersistentArrayMap
                clojure.lang.PersistentTreeMap]
  (fn map-encoder [m]
    (bytes-to-base64 (nippy/freeze m)))

  (fn map-decoder [s]
    (nippy/thaw (base64-to-bytes s))))

Nippy also automatically handles keys ordering:

(let [encoding1 (:encoding (c/check-path-encoding {{:a 1, :b 2} "map1"}))
      encoding2 (:encoding (c/check-path-encoding {{:b 2, :a 1} "map1"}))]
  (= encoding1 encoding2))

;=> true

Could something like this be added to Codax?
Could it leverage other types defined for pathwise?

Gracefully upgrade from a read to write transaction

Would it be possible to offer a way to upgrade a read transaction to a write transaction without starting the transaction from scratch?

It would be very helpful when chaining functions where a write might occur. Especially given the superior speed of read transactions.

Function "open-database" is difficult to use within REPL

I have following code:

(defn init-db [ _db_ ]
  (c/assoc-at! _db_ [:counters :user-id] 1))

(def DB (atom nil))

(defn prepare-all [tests]
  (if-not @DB
    (reset! DB (c/open-database "db.test")))
  (init-db @DB))

(use-fixtures :once prepare-all)

As you see this code prepares database connection for tests. In my normal workflow I recompile this class multiple times and try running tests each time. All goes well but after recompiling the variable DB is redefined and opened connection is lost somewhere.

Your function has all of them stored:

(defn open-database [path & [backup-fn]]
  (let [path (to-canonical-path-string path)]
    (when (@open-databases path)
      (throw (ex-info "Database Already Open" {:cause :database-open
                                               :message "The database at this path is already open."
                                               :path path})))

So while I've lost the connection in my REPL it still physically exists somewhere and I cannot re-open the database again. I don't know how to fix this, maybe you can provide a function like "current-connection" accepting the database name so that I can obtain the existing connection and continue working without re-opening the DB?
Thanks!

Extend support for java.time

As discussed in #4 and #7 the java.time support should be extended from only Instants to include OffsetDateTimes and ZonedDateTimes as well. Ideally all with the same hex tag so that they sort together.

Remove clj-time (and Joda-Time) dependency

Joda-Time was nice a few years back, but nowadays Clojure users are expected to use java.util.Date (mostly via #inst) and java.time.instant.

Most importantly, clj-time is deprecated. It's a little annoying to bring those dependencies in new projects.

Return only keys?

I have multiple projects for which I only want to know their ID (keys).

Usually I would simply do this (or the Codax equivalent):

(keys projects-db)

Once I have the keys I could use c/get-at to only fetch the information I want.

However this is taking way too much time, most probably because Codax is loading the entire map even when I only need the keys.
Is there an alternative approach to obtain the same info without loading everything?

Support for java.time Instants

The author of joda time has contributed java.time, and considers it an improved effort. Modern clojure libs are also preferring java.time.

What are the steps we can take to add support for this type as keys in the db map?

Issue in with-write-transaction

I'm doing like this:

(defn init-db [ db ]
(c/with-write-transaction [db tx]
(when-not (c/get-at tx [:counters :user-id])
(c/assoc-at tx [:counters :user-id] 1))))

This is for the one-time DB initialization. As you see, if the DB was already initialized, the body of the when-not will not be evaluated, nothing is written to DB. This leads to NullPointerException clojure.lang.RT.longCast (RT.java:1259)

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.