Code Monkey home page Code Monkey logo

steno's Introduction

Steno Node.js CI

Super fast async file writer with atomic write

Steno makes writing concurrently fast and safe.

Battle-tested in lowdb.

Features

  • ⚡ Fast (see benchmark)
  • 🐦 Lightweight (~6kb)
  • 🩵 Written in TypeScript
  • 🖊️ Writes are atomic (no partial writes)
  • 🏎️ Writes are ordered even if they're async (no race conditions)
  • ♻️ Automatic retry

Usage

import { Writer } from 'steno'

// Create a singleton writer
const file = new Writer('file.txt')

// Use it in the rest of your code
async function save() {
  await file.write('some data')
}

Benchmark

npm run benchmark (see src/benchmark.ts)

Write 1KB data to the same file x 1000
  fs     :   62ms
  steno  :    1ms

Write 1MB data to the same file x 1000
  fs     : 2300ms
  steno  :    5ms

Steno uses a smart queue and avoids unnecessary writes.

Name

https://en.wikipedia.org/wiki/Stenotype

License

MIT - Typicode

steno's People

Contributors

3imed-jaberi avatar dependabot[bot] avatar fritx avatar kiprasmel avatar luckydrq avatar pi0 avatar sgtpooki avatar thrashr888 avatar tonluong avatar typicode 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

steno's Issues

realpathSync causes error

Error: ENOENT: no such file or directory, lstat 'C:\projects\xxx\a.json'
    at Object.realpathSync (fs.js:1545:7)
    at new Writer (C:\projects\xxx\node_modules\steno\index.js:13:18)

I think this error is caused by #14
revert to previous version fixed the error.

Should `filename` be absolutized?

I just noticed that filename path stored in writers are not absolutized (like /home/xx or f:\xx)
And wouldn't it break our original intention of organization upon writers?
Btw, I think this is an awesome job!!

Could not resolve "node:path"

From LowDB:

✘ [ERROR] Could not resolve "node:path"

    node_modules/steno/lib/index.js:14:40:
      14 │ import { basename, dirname, join } from 'node:path';
         ╵                                         ~~~~~~~~~~~

  You can mark the path "node:path" as external to exclude it from the bundle, which will remove this error.


Build failed with 6 errors:
app/components/ServerWorker.ts:3:44: ERROR: Could not resolve "lowdb/node"
node_modules/lowdb/lib/adapters/TextFile.js:13:15: ERROR: Could not resolve "node:fs/promises"
node_modules/lowdb/lib/adapters/TextFileSync.js:13:15: ERROR: Could not resolve "node:fs"
node_modules/lowdb/lib/adapters/TextFileSync.js:14:17: ERROR: Could not resolve "node:path"
node_modules/steno/lib/index.js:13:34: ERROR: Could not resolve "node:fs/promises"

Whats wrong? Or whats mising?

Steno on Deno

This is just an announcement (discussions are not enabled in this repo 😞) for who would like to use Steno on Deno 🦕.

Just to try, I decided to "transform" ⚙️ Steno into a Deno version (whose I called Sdeno 😮).
I published it on GitHub ☮️ and on Deno Packages 📦!
Feel free to contribute!!! 😉


Here are all the links 🔗:
https://github.com/Bellisario/sdeno
https://deno.land/x/sdeno

Why all callbacks are called before next write is issued?

  1. Issue a write
  2. It will put callback in callbacks
  3. Write hit disc, we are waiting for fs.writeFile to complete
  4. Issue another write
  5. It will put another callback to callbacks
  6. First write completed, rename completed
  7. Now we calling all callbacks, but only first one should be called

Situation gets worse when you issue multiple writes after stuck one - there will never be any write for that call, so it's not clear when callback should be called. I think it should when his write or first one after is completed. Could be implemented with two lists of callbacks, one for current write, and one for next.

tsconfig: exported as commonjs module

Thanks for your wonderful packages!

I ran into an issue with steno, using vite / svelte, namely that steno is exported as a commonjs module.
My issue is fixed when I add

"module": "ES2020",

to "tsconfig.json".

(Not sure if you intended to export steno.js as commonjs)

Best,

Edwin

Error on Textfile

const { Writer } = require('steno');
^

Error [ERR_REQUIRE_ESM]: require() of ES Module C:\Users\Administrator\RaidenShogun-Bot\node_modules\steno\lib\index.js from C:\Users\Administrator\RaidenShogun-Bot\lib\lowdb\adapters\TextFile.js not supported.
Instead change the require of index.js in C:\Users\Administrator\RaidenShogun-Bot\lib\lowdb\adapters\TextFile.js to a dynamic import() which is available in all CommonJS modules.
at Object. (C:\Users\Administrator\RaidenShogun-Bot\lib\lowdb\adapters\TextFile.js:2:20)
at Object. (C:\Users\Administrator\RaidenShogun-Bot\lib\lowdb\adapters\JSONFile.js:1:22)
at Object. (C:\Users\Administrator\RaidenShogun-Bot\lib\lowdb\index.js:2:8)
at Object. (C:\Users\Administrator\RaidenShogun-Bot\main.js:19:9) {
code: 'ERR_REQUIRE_ESM'
}

[FR] Symlink support

Edit: I've created a fix for this - see #14


I'm using lowdb.

My database looks like this:

data/2020-03-10T18:07:39.933Z.json
data/2020-03-10T19:07:39.933Z.json
...
data/latest.json

latest.json is a symlink which points to the latest database file.

Every time I call db.setState, I previously unlink the latest.json file, create a new data/${new Date.toISOString()}.json file and point the latest.json file to it.

I want to be able to just use the latest.json symlink to provide myself convenience & be able to not worry about the underlying latest file.

But, as I've come to a conclusion - this does not work with lowdb, specifically because of how steno writes to file:

steno/index.js

Lines 31 to 62 in 94d1bb0

// Write data to a temporary file
var tmpFile = getTempFile(this.file)
fs.writeFile(tmpFile, data, function (err) {
if (err) {
// On error, call all the stored callbacks and the current one
// Then return
while (this.callbacks.length) this.callbacks.shift()(err)
cb(err)
return
}
// On success rename the temporary file to the real file
fs.rename(tmpFile, this.file, function (err) {
// call all the stored callbacks and the current one
while (this.callbacks.length) this.callbacks.shift()(err)
cb()
// Unlock file
this.lock = false
// Write next data if any
if (this.nextData) {
var data = this.nextData
this.callbacks = this.nextCallbacks
this.nextData = null
this.nextCallbacks = []
this.write(data, this.callbacks.pop())
}
}.bind(this))
}.bind(this))

by creating a temporary file, writing to it, and then renaming it to the "real file".

The issue is that there's no handling for symlinks.

Not safe at all

This module isn't "safe" at all because it doesn't use any kind of cross-process locking. If you use it with cluster it's unsafe. If you use it with multiple processes it's unsafe. It's only safe when you assume a single process.

What you're looking for is flock() from fs-ext.

Overwrite leads to non-deterministic behaviour

The README says:

steno('file.txt').write('C') // still writing A, B is replaced by C

Why on earth would I want C to overwrite B? I expect all f.write operations to queue and complete in order, not for stuff to be silently dropped with no errors! This gives non-deterministic behaviour, e.g.

var file_handle = steno('file.txt');
file_handle.write(A)
var B = do_something();
file_handle.write(B);
var C = do_something_else();
file_handle.write(C);

I now have no idea whether my file looks like "ABC" or "AB" because this depends on the completion times of .write(B) and do_something_else()...

Are benchmarks realistic?

Hi there 👋 First of all - thanks for all your great work, I appreciate it!

I just wanted to ask something I can't wrap my head around. It seems steno is way way faster than regular fs. But then I just saw it is internally using the fs itself, and it got me wondering - well how is that possible?

And I'm actually thinking the benchmark is comparing apples to oranges. From benchmark.ts:

// To avoid race condition issues, we need to wait
// between write when using fs only
for (let i = 0; i < 1000; i++) {
  await writeFile(fsFile, `${data}${i}`)
}
console.timeEnd(fsLabel)

console.time(stenoLabel)
// Steno can be run in parallel
await Promise.all(
  [...Array(1000).keys()].map((_, i) => steno.write(`${data}${i}`)),
)
console.timeEnd(stenoLabel)

What I see here is that the first one actually does write into a file a thousand times. It waits for each write to finish and then runs the next one.

The second benchmark runs all writes in parallel (as stated). That's the first issue that makes me think comparison is not proper. But the second one is even more important and it comes from the lock implementation (index.ts):

async write(data: string): Promise<void> {
  return this.#locked ? this.#add(data) : this.#write(data)
}

The way this locking mechanism is implemented is that it would "save" (#add) data for later usage, if a write is currently ongoing. When the first write is done, it will check if there's any next data to be written to the file, and if so - makes the call:

if (this.#nextData !== null) {
  const nextData = this.#nextData
  this.#nextData = null
  await this.write(nextData)
}

Since #nextData is a single property that gets overridden (as stated in the code), you have just one next data to write, and you make a second file write call. And that's the end of the chain of writes.

What I'm saying is that from what I can see, you compare a thousand writes to a file, to just two writes to a file. Which is of course way way slower 😃

In no way I'm pointing fingers here, I'm just trying to figure out if I've missed something, or it's just a wrong statement.

Thanks again, good luck!

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.