Skip to content

Commit

Permalink
feat(node-fs): new package (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
alimd authored Jan 8, 2024
2 parents ef1fb8f + b7370d6 commit 347e70c
Show file tree
Hide file tree
Showing 17 changed files with 536 additions and 7 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ Here is a brief overview of the included libraries:
11. [`exit-hook`](./packages/exit-hook/README.md): A utility for registering exit handlers in Node.js.
12. [`flatomise`](./packages/flatomise/README.md): A utility for creating promises that can be externally resolved or rejected.
13. [`async-queue`](./packages/async-queue/README.md): A queue that executes async tasks in order like mutex and semaphore methodology for javascript and typescript.
14. [`node-fs`](./packages/node-fs/README.md): Enhanced file system operations in Node.js, including reading, writing, and handling JSON files, with both synchronous and asynchronous options.

For more detailed information and guidelines on how to use each package, please refer to to each package's README.
For more detailed information and guidelines on how to use each package, please refer to each package's README.
4 changes: 2 additions & 2 deletions packages/logger/src/define-package.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {definePackage as definePackage_} from '@alwatr/dedupe';

import {createLogger} from './logger.js';
import {createLogger} from './logger';

import type {AlwatrLogger} from './type.js';
import type {AlwatrLogger} from './type';

/**
* Global define package for managing package versions to prevent version conflicts and return package level logger.
Expand Down
2 changes: 1 addition & 1 deletion packages/logger/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {definePackage} from '@alwatr/dedupe';
import {platformInfo} from '@alwatr/platform-info';

import type {AlwatrLogger} from './type.js';
import type {AlwatrLogger} from './type';

definePackage('@alwatr/logger', __package_version__);

Expand Down
37 changes: 37 additions & 0 deletions packages/node-fs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Node FS

Enhanced file system operations in Node.js with asynchronous queue to prevent parallel writes.

## Installation

```bash
yarn add @alwatr/node-fs
```

## Features

- Checks if a directory exists. If it doesn't, it creates the directory and all necessary subdirectories.
- Before writing a file successfully, first writes it to a temporary path (`path.tmp`).
- If a file already exists, renames and keeps the existing file at a backup path (`path.bak`).
- If a write operation fails, the original file remains unchanged.
- Includes `readJson` and `writeJson` functions that automatically parse and stringify JSON data.
- Supports both synchronous and asynchronous read/write operations.
- An asynchronous queue is used to prevent simultaneous write operations.
- Fully written in TypeScript, includes type definitions.
- Separate builds are provided for ESModule and CommonJS.
- Zero dependencies, except for the nanolib library.
- Includes a beautiful log feature, which uses the [logger](https://github.com/Alwatr/nanolib/tree/next/packages/logger) package from nanolib.

## Usage

```typescript
import {writeJson} from '@alwatr/node-fs';

const path = 'file.json';
await writeJson(path, {a: 1}); // wait to finish
writeJson(path, {a: 2}); // asynchronous write in queue
writeJson(path, {a: 3}); // asynchronous write in queue

const data = await readJson(path); // automatically wait for the queue to finish
console.log(data.a); // 3
```
20 changes: 20 additions & 0 deletions packages/node-fs/demo/make-file-bench.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {existsSync, makeEmptyFile, resolve} from '@alwatr/node-fs';

import {mkdir, rm} from 'node:fs/promises';

(async () => {
const temp = resolve('./.tmp');

await mkdir(temp);

console.log('start bench');

console.time('bench');
for (let i = 0; i < 10_000; i++) {
await makeEmptyFile(`${temp}/file-${i}.asn`);
}
console.timeEnd('bench');

await rm(temp, {recursive: true, force: true});

})();
14 changes: 14 additions & 0 deletions packages/node-fs/demo/node-fs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {readJson, writeJson} from '@alwatr/node-fs';

for (let i = 0; i < 100; i++) {
console.log('writeJson %s without waiting...', i);
writeJson('file.json', {i, a: 'b'});
if (i===70) {
console.log('readJson %s', i);
console.dir(await readJson('file.json'));
}
}

console.dir(await readJson('file.json'));

console.log('loop done, wait for queue process')
88 changes: 88 additions & 0 deletions packages/node-fs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{
"name": "@alwatr/node-fs",
"version": "0.0.0",
"description": "Enhanced file system operations in Node.js with asynchronous queue to prevent parallel writes.",
"author": "S. Ali Mihandoost <[email protected]>",
"keywords": [
"node-fs",
"fs",
"file",
"filesystem",
"readFile",
"writeFile",
"readJson",
"writeJson",
"JSON",
"async",
"queue",
"cross-platform",
"ECMAScript",
"typescript",
"javascript",
"node",
"nodejs",
"esm",
"module",
"utility",
"util",
"utils",
"nanolib",
"alwatr"
],
"type": "module",
"main": "./dist/main.cjs",
"module": "./dist/main.mjs",
"types": "./dist/main.d.ts",
"exports": {
".": {
"import": "./dist/main.mjs",
"require": "./dist/main.cjs",
"types": "./dist/main.d.ts"
}
},
"license": "MIT",
"files": [
"**/*.{js,mjs,cjs,map,d.ts,html,md}",
"!demo/**/*"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/Alwatr/nanolib",
"directory": "packages/node-fs"
},
"homepage": "https://github.com/Alwatr/nanolib/tree/next/packages/node-fs#readme",
"bugs": {
"url": "https://github.com/Alwatr/nanolib/issues"
},
"prettier": "@alwatr/prettier-config",
"scripts": {
"b": "yarn run build",
"w": "yarn run watch",
"c": "yarn run clean",
"cb": "yarn run clean && yarn run build",
"d": "yarn run build:es && yarn node --enable-source-maps --trace-warnings",
"build": "yarn run build:ts & yarn run build:es",
"build:es": "nano-build --preset=module",
"build:ts": "tsc --build",
"watch": "yarn run watch:ts & yarn run watch:es",
"watch:es": "yarn run build:es --watch",
"watch:ts": "yarn run build:ts --watch --preserveWatchOutput",
"clean": "rm -rfv dist *.tsbuildinfo"
},
"dependencies": {
"@alwatr/async-queue": "workspace:^",
"@alwatr/flat-string": "workspace:^",
"@alwatr/logger": "workspace:^"
},
"devDependencies": {
"@alwatr/nano-build": "workspace:^",
"@alwatr/prettier-config": "workspace:^",
"@alwatr/tsconfig-base": "workspace:^",
"@alwatr/type-helper": "workspace:^",
"@types/node": "^20.10.7",
"typescript": "^5.3.3"
}
}
8 changes: 8 additions & 0 deletions packages/node-fs/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {AsyncQueue} from '@alwatr/async-queue';
import {definePackage} from '@alwatr/logger';

import type {} from '@alwatr/nano-build';

export const logger = definePackage('@alwatr/node-fs', __package_version__);

export const asyncQueue = new AsyncQueue();
45 changes: 45 additions & 0 deletions packages/node-fs/src/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {logger} from './common';

import type { Dictionary } from '@alwatr/type-helper';

/**
* Parse json string.
*
* @param content - json string
* @returns json object
* @example
* ```typescript
* const json = parseJson('{"a":1,"b":2}');
* console.log(json.a); // 1
* ```
*/
export function parseJson<T extends Dictionary>(content: string): T {
try {
return JSON.parse(content);
}
catch (err) {
logger.error('parseJson', 'invalid_json', err);
throw new Error('invalid_json', {cause: (err as Error).cause});
}
}

/**
* Stringify json object.
*
* @param data - json object
* @returns json string
* @example
* ```typescript
* const json = jsonStringify({a:1, b:2});
* console.log(json); // '{"a":1,"b":2}'
* ```
*/
export function jsonStringify<T extends Dictionary>(data: T): string {
try {
return JSON.stringify(data);
}
catch (err) {
logger.error('jsonStringify', 'stringify_failed', err);
throw new Error('stringify_failed', {cause: (err as Error).cause});
}
}
9 changes: 9 additions & 0 deletions packages/node-fs/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './read-file';
export * from './write-file';
export * from './read-json';
export * from './write-json';
export * from './make-file';

export {resolve} from 'node:path';
export {existsSync} from 'node:fs';
export {unlink} from 'node:fs/promises';
18 changes: 18 additions & 0 deletions packages/node-fs/src/make-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {open} from 'node:fs/promises';

import {logger} from './common';

/**
* Make empty file.
*
* @param path - file path
*
* @example
* ```ts
* await makeFile('./file.txt');
* ```
*/
export async function makeEmptyFile(path: string): Promise<void> {
logger.logMethodArgs?.('makeEmptyFile', '...' + path.slice(-32));
return (await open(path, 'w')).close();
}
54 changes: 54 additions & 0 deletions packages/node-fs/src/read-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {readFileSync as readFileSync_} from 'node:fs';
import {readFile as readFile_} from 'node:fs/promises';

import {flatString} from '@alwatr/flat-string';

import {asyncQueue, logger} from './common';

/**
* Enhanced read File (Synchronous).
*
* @param path - file path
* @returns file content
* @example
* ```typescript
* const fileContent = readFileSync('./file.txt', sync);
* ```
*/
export function readFileSync(path: string): string {
logger.logMethodArgs?.('readFileSync', '...' + path.slice(-32));
// if (!existsSync(path)) throw new Error('file_not_found');
try {
return flatString(readFileSync_(path, {encoding: 'utf-8', flag: 'r'}));
}
catch (err) {
logger.error('readFileSync', 'read_file_failed', {path}, err);
throw new Error('read_file_failed', {cause: (err as Error).cause});
}
}

/**
* Enhanced read File (Asynchronous).
*
* - If writing queue is running for target path, it will wait for it to finish.
*
* @param path - file path
* @returns file content
* @example
* ```typescript
* const fileContent = await readFile('./file.txt', sync);
* ```
*/
export function readFile(path: string): Promise<string> {
logger.logMethodArgs?.('readFile', '...' + path.slice(-32));
// if (!existsSync(path)) throw new Error('file_not_found');
return asyncQueue.push(path, async () => {
try {
return flatString(await readFile_(path, {encoding: 'utf-8', flag: 'r'}));
}
catch (err) {
logger.error('readFile', 'read_file_failed', {path}, err);
throw new Error('read_file_failed', {cause: (err as Error).cause});
}
});
}
61 changes: 61 additions & 0 deletions packages/node-fs/src/read-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {logger} from './common';
import {parseJson} from './json';
import {readFile, readFileSync} from './read-file';

import type {Dictionary, MaybePromise} from '@alwatr/type-helper';

/**
* Enhanced read json file (async).
*
* @param path - file path
* @returns json object
* @example
* ```typescript
* const fileContent = await readJson('./file.json');
* ```
*/
export function readJson<T extends Dictionary>(path: string): Promise<T>;
/**
* Enhanced read json file (sync).
*
* @param path - file path
* @param sync - sync mode
* @returns json object
* @example
* ```typescript
* const fileContent = readJson('./file.json', true);
* ```
*/
export function readJson<T extends Dictionary>(path: string, sync: true): T;
/**
* Enhanced read json file.
*
* @param path - file path
* @param sync - sync mode
* @returns json object
* @example
* ```typescript
* const fileContent = await readJson('./file.json', sync);
* ```
*/
export function readJson<T extends Dictionary>(path: string, sync: boolean): MaybePromise<T>;
/**
* Enhanced read json file.
*
* @param path - file path
* @param sync - sync mode
* @returns json object
* @example
* ```typescript
* const fileContent = await readJson('./file.json');
* ```
*/
export function readJson<T extends Dictionary>(path: string, sync = false): MaybePromise<T> {
logger.logMethodArgs?.('readJson', {path: path.slice(-32), sync});
if (sync === true) {
return parseJson<T>(readFileSync(path));
}
else {
return readFile(path).then((content) => parseJson<T>(content));
}
}
Loading

0 comments on commit 347e70c

Please sign in to comment.