Skip to content

Commit 1a136c1

Browse files
committed
Add atomic pushView function
1 parent a467500 commit 1a136c1

3 files changed

Lines changed: 38 additions & 27 deletions

File tree

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Public analytics as a Node.js microservice.
44

5-
With less than 50 lines of code this service is the smallest analytics you'll ever need. It does nothing except count the views of something and making the views accessible via an API.
5+
With less than 100 lines of code this service is the smallest analytics you'll ever need. It does nothing except count the views of something and making the views accessible via an API.
66

77
(there is currently no frontend to consume the statistics, though writing one is on the to-do list)
88

@@ -39,10 +39,6 @@ This is how you'd track pageviews for a website: (though note that this can be u
3939
</script>
4040
```
4141

42-
## Gotchas
43-
44-
Currently this uses promisified `level` for data storage, which doesn't have atomic operations. If you have a recommendation for a filesystem-based data storage module for Node that has atomic operations, please let us know!
45-
4642
## Contributing
4743

4844
If you run `npm run dev` the server will restart every time you edit the code. Perfect for development of `micro-analytics`!

db.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const promise = require('promise')
33

44
const db = flatfile.sync('views')
55

6-
module.exports = {
6+
const promisifiedDb = {
77
// Promisify async operations
88
put: promise.denodeify(db.put.bind(db)),
99
del: promise.denodeify(db.del.bind(db)),
@@ -14,3 +14,32 @@ module.exports = {
1414
keys: db.keys.bind(db),
1515
close: db.close.bind(db),
1616
}
17+
18+
// Atomic view pushing method, can safely be called multiple times without the
19+
// db messing up.
20+
const pushView = async (key, view) => {
21+
const locks = {}
22+
await push()
23+
24+
async function push() {
25+
if (locks[key]) return setImmediate(push)
26+
locks[key] = true
27+
28+
let views
29+
if (promisifiedDb.has(key)) {
30+
views = promisifiedDb.get(key).views
31+
} else {
32+
views = []
33+
}
34+
35+
try {
36+
await promisifiedDb.put(key, { views: views.concat([view]) })
37+
delete locks[key]
38+
} catch (err) {
39+
throw err
40+
}
41+
}
42+
}
43+
44+
module.exports = exports = promisifiedDb
45+
exports.pushView = pushView

index.js

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,14 @@ module.exports = async function (req, res) {
1414
}
1515
const shouldIncrement = query.inc !== 'false' && query.inc !== false
1616
try {
17-
if (db.has(pathname)) {
18-
const { views } = await db.get(pathname)
19-
// Add a view and send the total views back to the client
20-
if (shouldIncrement) {
21-
views.push({ time: Date.now() })
22-
await db.put(pathname, { views })
23-
}
24-
if (req.method === 'GET') {
25-
send(res, 200, { views: views.length })
26-
} else {
27-
send(res, 200)
28-
}
17+
// Add a view and send the total views back to the client
18+
if (shouldIncrement) {
19+
await db.pushView(pathname, { time: Date.now() })
20+
}
21+
if (req.method === 'GET') {
22+
send(res, 200, { views: db.get(pathname).views.length })
2923
} else {
30-
// Initialise the page with one view
31-
if (shouldIncrement) {
32-
await db.put(pathname, { views: [{ time: Date.now() }] })
33-
}
34-
if (req.method === 'GET') {
35-
send(res, 200, { views: shouldIncrement ? 1 : 0 })
36-
} else {
37-
send(res, 200)
38-
}
24+
send(res, 200)
3925
}
4026
} catch (err) {
4127
console.log(err)

0 commit comments

Comments
 (0)