Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(node-fs): new package #29

Merged
merged 24 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5c23e01
feat(node-fs): copy from store
njfamirm Jan 6, 2024
74cbe48
feat(node-fs): base package
njfamirm Jan 6, 2024
31f3740
feat(node-fs): demo
njfamirm Jan 6, 2024
3880703
feat(node-fs): definePackage and logger
njfamirm Jan 7, 2024
d9d49b4
docs: fix typo
njfamirm Jan 8, 2024
186ba09
feat(node-fs): makeFile
njfamirm Jan 8, 2024
a2b15a4
refactor(node-fs): separate files
alimd Jan 8, 2024
a39590b
refactor(node-fs): rewrite makeEmptyFile
alimd Jan 8, 2024
ca3a57a
feat(node-fs): Add make-file-bench demo script
alimd Jan 8, 2024
7bddaa0
fix(node-fs): Update import statement for make-file module
alimd Jan 8, 2024
7534ed1
feat(node-fs): rewrite new writeFile method
alimd Jan 8, 2024
581d4f9
feat(node-fs): writeJson method
alimd Jan 8, 2024
69da5b7
chore(node-fs): review and test
alimd Jan 8, 2024
6d8b3d7
feat(node-fs): writeFile under asyncQueue
alimd Jan 8, 2024
f620739
chore(logger): Fix import file extensions
alimd Jan 8, 2024
5616b53
refactor(node-fs): separate readJson
alimd Jan 8, 2024
de2cf5d
refactor(node-fs): separate read file functions
alimd Jan 8, 2024
e85d927
feat(node-fs): enhance json types
alimd Jan 8, 2024
e9cf12d
refactor(node-fs): readJson function
alimd Jan 8, 2024
faeef41
chore(logger): Fix import file extensions
alimd Jan 8, 2024
ab12153
feat(node-fs): readFile under asyncQueue
alimd Jan 8, 2024
9010c72
feat(node-fs): enhance writeJson type
alimd Jan 8, 2024
8fc2749
docs(node-fs): readme and desc
alimd Jan 8, 2024
b7370d6
chore(node-fs): enhance demo test
alimd Jan 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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