@wxn0brp/db-storage-bin 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ name: Build
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ tags:
8
+ - "*"
9
+
10
+ workflow_dispatch:
11
+
12
+ concurrency:
13
+ group: build
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ build:
18
+ uses: wxn0brP/workflow-dist/.github/workflows/build-ts.yml@main
19
+ with:
20
+ scriptsHandling: "remove-all"
21
+ publishToNpm: true
22
+
23
+ secrets:
24
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
+
5
+ ### 0.0.2 (2025-08-24)
6
+
7
+
8
+ ### Features
9
+
10
+ * check file integrality ([e2b1aac](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/e2b1aacddfd5d3cb8a21bb4400827608c361e9ed))
11
+ * update ([cc2104c](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/cc2104c8dace53180efc76365930d63dd917d383))
12
+ * update ([4e5a9a8](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/4e5a9a82ee01f96084d27b1cfd893e129d6675b7))
13
+ * update ([fbec71a](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/fbec71a7cff65ed221ddbd7956bc252862e87760))
14
+ * update ([e3b8edb](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/e3b8edb5c7f833ce0a948be878173e207946229b))
15
+ * update ([65d73e4](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/65d73e4034944ecaa5963aa79d16a7fef795ce26))
16
+ * update ([baebb1f](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/baebb1f4258303133ec315f72cffaefdbf252600))
17
+ * update ([5f7c905](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/5f7c9053e97620cd2a5db4f9f703f5f8009f78ed))
18
+ * update ([60d0347](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/60d0347b6f3ff562d851aee842c090d081ab0a8c))
19
+ * update logs ([d79bd84](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/d79bd8439111f43ad4c99e44c860516695dc3123))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * capacity ([05de295](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/05de2951c005f98697e067e3857a331777110110))
25
+ * data len int32 to uint32 ([534a3b0](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/534a3b07d465320c0334a2fa557fb4bc070e7139))
26
+ * optimize ([d704727](https://github.com/wxn0brp/ValtheraDB-storage-bin/commit/d7047276d31ccba5425de32fc28a8a65fcd5e02f))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wxn0brP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # ValtheraDB Bin Plugin
2
+
3
+ This is a proof-of-concept for an addon/plugin for the `@wxn0brp/db` (ValtheraDB) library.
4
+
5
+ The purpose of this experiment is to create a storage layer that allows ValtheraDB, which normally operates on a directory/file structure, to instead use a single binary file for data storage.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ yarn add github:wxn0brP/ValtheraDB-storage-bin#dist
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Initialization
16
+
17
+ To get started, create a new `BinValthera` instance:
18
+
19
+ ```typescript
20
+ import { createBinValthera } from "@wxn0brp/db-storage-bin";
21
+
22
+ const { db, actions, mgr } = await createBinValthera("test.val", { preferredSize: 4096 });
23
+ ```
24
+
25
+ This will create a new binary file named `test.val` (if it doesn't exist) and initialize the database.
26
+
27
+ ### Basic
28
+
29
+ The `db` object is an instance of `ValtheraClass` and supports the standard ValtheraDB methods.
30
+
31
+ ### Optimizing the Database
32
+
33
+ This will reclaim unused space in the binary file.
34
+
35
+ ```typescript
36
+ await mgr.optimize();
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `createBinValthera(path, opts, init)`
42
+
43
+ - `path`: The path to the binary file.
44
+ - `opts`: Options for the `BinManager`.
45
+ - `init`: Whether to initialize the database upon creation (default: `true`).
46
+
47
+ Returns an object containing:
48
+ - `db`: An instance of `ValtheraClass`.
49
+ - `actions`: An instance of `BinFileAction`.
50
+ - `mgr`: An instance of `BinManager`.
51
+
52
+ ### `BinManager(path, options)`
53
+
54
+ - `path`: The path to the binary file.
55
+ - `options`:
56
+ - `preferredSize`: The preferred block size for the database (default: `256`).
57
+
58
+ ## License
59
+
60
+ This project is licensed under the MIT License.
@@ -0,0 +1,64 @@
1
+ import dbActionBase from "@wxn0brp/db-core/base/actions";
2
+ import Data from "@wxn0brp/db-core/types/data";
3
+ import FileCpu from "@wxn0brp/db-core/types/fileCpu";
4
+ import { DbOpts } from "@wxn0brp/db-core/types/options";
5
+ import { VQuery } from "@wxn0brp/db-core/types/query";
6
+ import { BinManager } from "./bin/index.js";
7
+ export declare class BinFileAction extends dbActionBase {
8
+ private mgr;
9
+ folder: string;
10
+ options: DbOpts;
11
+ fileCpu: FileCpu;
12
+ /**
13
+ * Creates a new instance of dbActionC.
14
+ * @constructor
15
+ * @param folder - The folder where database files are stored.
16
+ * @param options - The options object.
17
+ */
18
+ constructor(mgr: BinManager);
19
+ init(): Promise<void>;
20
+ /**
21
+ * Get a list of available databases in the specified folder.
22
+ */
23
+ getCollections(): Promise<string[]>;
24
+ /**
25
+ * Check and create the specified collection if it doesn't exist.
26
+ */
27
+ ensureCollection({ collection }: VQuery): Promise<boolean>;
28
+ /**
29
+ * Check if a collection exists.
30
+ */
31
+ issetCollection({ collection }: VQuery): Promise<boolean>;
32
+ /**
33
+ * Add a new entry to the specified database.
34
+ */
35
+ add({ collection, data, id_gen }: VQuery): Promise<import("@wxn0brp/db-core/types/arg").Arg>;
36
+ /**
37
+ * Find entries in the specified database based on search criteria.
38
+ */
39
+ find(query: VQuery): Promise<Data[]>;
40
+ /**
41
+ * Find the first matching entry in the specified database based on search criteria.
42
+ */
43
+ findOne({ collection, search, context, findOpts }: VQuery): Promise<Data>;
44
+ /**
45
+ * Update entries in the specified database based on search criteria and an updater function or object.
46
+ */
47
+ update({ collection, search, updater, context }: VQuery): Promise<boolean>;
48
+ /**
49
+ * Update the first matching entry in the specified database based on search criteria and an updater function or object.
50
+ */
51
+ updateOne({ collection, search, updater, context }: VQuery): Promise<boolean>;
52
+ /**
53
+ * Remove entries from the specified database based on search criteria.
54
+ */
55
+ remove({ collection, search, context }: VQuery): Promise<boolean>;
56
+ /**
57
+ * Remove the first matching entry from the specified database based on search criteria.
58
+ */
59
+ removeOne({ collection, search, context }: VQuery): Promise<boolean>;
60
+ /**
61
+ * Removes a database collection from the file system.
62
+ */
63
+ removeCollection({ collection }: VQuery): Promise<boolean>;
64
+ }
@@ -0,0 +1,106 @@
1
+ import { CustomFileCpu, genId } from "@wxn0brp/db-core";
2
+ import dbActionBase from "@wxn0brp/db-core/base/actions";
3
+ import { findUtil } from "@wxn0brp/db-core/utils/action";
4
+ export class BinFileAction extends dbActionBase {
5
+ mgr;
6
+ folder;
7
+ options;
8
+ fileCpu;
9
+ /**
10
+ * Creates a new instance of dbActionC.
11
+ * @constructor
12
+ * @param folder - The folder where database files are stored.
13
+ * @param options - The options object.
14
+ */
15
+ constructor(mgr) {
16
+ super();
17
+ this.mgr = mgr;
18
+ this.fileCpu = new CustomFileCpu(this.mgr.read.bind(this.mgr), this.mgr.write.bind(this.mgr));
19
+ }
20
+ async init() {
21
+ await this.mgr.open();
22
+ }
23
+ /**
24
+ * Get a list of available databases in the specified folder.
25
+ */
26
+ async getCollections() {
27
+ const collections = this.mgr.meta.collections.map(c => c.name);
28
+ return collections;
29
+ }
30
+ /**
31
+ * Check and create the specified collection if it doesn't exist.
32
+ */
33
+ async ensureCollection({ collection }) {
34
+ if (await this.issetCollection(arguments[0]))
35
+ return;
36
+ await this.mgr.write(collection, []);
37
+ return true;
38
+ }
39
+ /**
40
+ * Check if a collection exists.
41
+ */
42
+ async issetCollection({ collection }) {
43
+ const collections = await this.getCollections();
44
+ return collections.includes(collection);
45
+ }
46
+ /**
47
+ * Add a new entry to the specified database.
48
+ */
49
+ async add({ collection, data, id_gen = true }) {
50
+ await this.ensureCollection(arguments[0]);
51
+ if (id_gen)
52
+ data._id = data._id || genId();
53
+ await this.fileCpu.add(collection, data);
54
+ return data;
55
+ }
56
+ /**
57
+ * Find entries in the specified database based on search criteria.
58
+ */
59
+ async find(query) {
60
+ await this.ensureCollection(query);
61
+ return await findUtil(query, this.fileCpu, [query.collection]);
62
+ }
63
+ /**
64
+ * Find the first matching entry in the specified database based on search criteria.
65
+ */
66
+ async findOne({ collection, search, context = {}, findOpts = {} }) {
67
+ await this.ensureCollection(arguments[0]);
68
+ let data = await this.fileCpu.findOne(collection, search, context, findOpts);
69
+ return data || null;
70
+ }
71
+ /**
72
+ * Update entries in the specified database based on search criteria and an updater function or object.
73
+ */
74
+ async update({ collection, search, updater, context = {} }) {
75
+ await this.ensureCollection(arguments[0]);
76
+ return await this.fileCpu.update(collection, false, search, updater, context);
77
+ }
78
+ /**
79
+ * Update the first matching entry in the specified database based on search criteria and an updater function or object.
80
+ */
81
+ async updateOne({ collection, search, updater, context = {} }) {
82
+ await this.ensureCollection(arguments[0]);
83
+ return await this.fileCpu.update(collection, true, search, updater, context);
84
+ }
85
+ /**
86
+ * Remove entries from the specified database based on search criteria.
87
+ */
88
+ async remove({ collection, search, context = {} }) {
89
+ await this.ensureCollection(arguments[0]);
90
+ return await this.fileCpu.remove(collection, false, search, context);
91
+ }
92
+ /**
93
+ * Remove the first matching entry from the specified database based on search criteria.
94
+ */
95
+ async removeOne({ collection, search, context = {} }) {
96
+ await this.ensureCollection(arguments[0]);
97
+ return await this.fileCpu.remove(collection, true, search, context);
98
+ }
99
+ /**
100
+ * Removes a database collection from the file system.
101
+ */
102
+ async removeCollection({ collection }) {
103
+ await this.mgr.removeCollection(collection);
104
+ return true;
105
+ }
106
+ }
@@ -0,0 +1,6 @@
1
+ import { BinManager, CollectionMeta } from "./index.js";
2
+ import { FileMeta } from "./head.js";
3
+ export declare function findCollection(cmp: BinManager, name: string): CollectionMeta | undefined;
4
+ export declare function findFreeSlot(cmp: BinManager, size: number): Promise<FileMeta["freeList"][number] | undefined>;
5
+ export declare function writeLogic(cmp: BinManager, collection: string, data: object[]): Promise<void>;
6
+ export declare function readLogic(cmp: BinManager, collection: string): Promise<any>;
@@ -0,0 +1,80 @@
1
+ import { _log } from "../log.js";
2
+ import { saveHeaderAndPayload } from "./head.js";
3
+ import { detectCollisions, pushToFreeList, readData, roundUpCapacity, writeData } from "./utils.js";
4
+ export function findCollection(cmp, name) {
5
+ return cmp.meta.collections.find(c => c.name === name);
6
+ }
7
+ export async function findFreeSlot(cmp, size) {
8
+ const { meta } = cmp;
9
+ await _log(6, "Finding free slot for size:", size);
10
+ const idx = meta.freeList.findIndex(f => f.capacity >= size);
11
+ if (idx === -1) {
12
+ await _log(6, "No suitable free slot found.");
13
+ return undefined;
14
+ }
15
+ const slot = meta.freeList[idx];
16
+ await _log(6, "Free slot found at index:", idx, "with capacity:", slot.capacity);
17
+ meta.freeList.splice(idx, 1);
18
+ await _log(6, "Slot removed from freeList:", slot);
19
+ return slot;
20
+ }
21
+ export async function writeLogic(cmp, collection, data) {
22
+ const { fd, meta } = cmp;
23
+ await _log(3, "Writing data to collection:", collection);
24
+ const existingCollection = findCollection(cmp, collection);
25
+ const encoded = Buffer.from(await cmp.options.format.encode(data));
26
+ const length = encoded.length;
27
+ const capacity = roundUpCapacity(meta, length + 4);
28
+ let offset = existingCollection?.offset;
29
+ let existingOffset = existingCollection?.offset;
30
+ let existingCapacity = existingCollection?.capacity;
31
+ const collision = detectCollisions(meta, offset, capacity, [collection]);
32
+ if (collision || !existingCollection) {
33
+ if (collision)
34
+ await _log(2, "Collision detected");
35
+ const slot = await findFreeSlot(cmp, capacity);
36
+ if (slot) {
37
+ offset = slot.offset;
38
+ await _log(4, "Found free slot at offset:", offset);
39
+ }
40
+ else {
41
+ offset = meta.fileSize;
42
+ meta.fileSize += capacity;
43
+ await _log(4, "No free slot found, appending at offset:", offset);
44
+ }
45
+ if (!existingCollection) {
46
+ meta.collections.push({ name: collection, offset, capacity });
47
+ }
48
+ else if (collision) {
49
+ pushToFreeList(meta, existingOffset, existingCapacity);
50
+ meta.collections = meta.collections.map(c => {
51
+ if (c.offset === existingOffset)
52
+ return { name: c.name, offset, capacity };
53
+ return c;
54
+ });
55
+ }
56
+ await _log(3, "Collection written");
57
+ await saveHeaderAndPayload(cmp);
58
+ }
59
+ const buf = Buffer.alloc(4);
60
+ buf.writeUInt32LE(length, 0);
61
+ await writeData(fd, offset, buf, 4);
62
+ await writeData(fd, offset + 4, encoded, capacity);
63
+ if (existingCollection && length >= existingCollection.capacity) {
64
+ meta.collections = meta.collections.map(c => {
65
+ if (c.offset === offset)
66
+ return { name: c.name, offset, capacity };
67
+ return c;
68
+ });
69
+ await saveHeaderAndPayload(cmp);
70
+ await _log(2, "Capacity exceeded");
71
+ }
72
+ }
73
+ export async function readLogic(cmp, collection) {
74
+ const collectionMeta = findCollection(cmp, collection);
75
+ if (!collectionMeta)
76
+ throw new Error("Collection not found");
77
+ const len = await readData(cmp.fd, collectionMeta.offset, 4);
78
+ const data = await readData(cmp.fd, collectionMeta.offset + 4, len.readUInt32LE(0));
79
+ return await cmp.options.format.decode(data);
80
+ }
@@ -0,0 +1,20 @@
1
+ import { BinManager, CollectionMeta } from "./index.js";
2
+ export interface Block {
3
+ offset: number;
4
+ capacity: number;
5
+ }
6
+ export interface FileMeta {
7
+ collections: CollectionMeta[];
8
+ freeList: Block[];
9
+ fileSize: number;
10
+ payloadLength: number;
11
+ payloadOffset: number;
12
+ blockSize: number;
13
+ }
14
+ export declare function openFile(cmp: BinManager): Promise<FileMeta>;
15
+ export declare function readHeaderPayload(cmp: BinManager): Promise<void>;
16
+ export declare function getHeaderPayload(meta: FileMeta): {
17
+ c: (string | number)[][];
18
+ f: number[][];
19
+ };
20
+ export declare function saveHeaderAndPayload(cmp: BinManager, recursion?: boolean): Promise<void>;
@@ -0,0 +1,135 @@
1
+ import { getFileCrc } from "../crc32.js";
2
+ import { _log } from "../log.js";
3
+ import { findFreeSlot } from "./data.js";
4
+ import { HEADER_SIZE, VERSION } from "./static.js";
5
+ import { detectCollisions, pushToFreeList, roundUpCapacity, writeData } from "./utils.js";
6
+ ;
7
+ export async function openFile(cmp) {
8
+ const { fd, options } = cmp;
9
+ const stats = await fd.stat();
10
+ const fileSize = stats.size;
11
+ await _log(2, "File size:", fileSize);
12
+ const meta = {
13
+ collections: [],
14
+ freeList: [],
15
+ fileSize,
16
+ payloadLength: 0,
17
+ payloadOffset: 0,
18
+ blockSize: options.preferredSize ?? 256,
19
+ };
20
+ cmp.meta = meta;
21
+ if (fileSize < HEADER_SIZE) {
22
+ await _log(2, "Initializing new file header");
23
+ await saveHeaderAndPayload(cmp);
24
+ await _log(6, "Header initialized with size:", HEADER_SIZE);
25
+ return meta;
26
+ }
27
+ const headerBuf = Buffer.alloc(HEADER_SIZE);
28
+ await fd.read(headerBuf, 0, HEADER_SIZE, 0);
29
+ await _log(6, "Header read from file");
30
+ const version = headerBuf.readUInt32LE(0);
31
+ if (version !== VERSION) {
32
+ await _log(6, "err", `Unsupported file version: ${version}`);
33
+ throw new Error(`Unsupported file version ${version}`);
34
+ }
35
+ await _log(2, "File version:", version);
36
+ const payloadLength = headerBuf.readUInt32LE(4);
37
+ meta.payloadLength = payloadLength;
38
+ await _log(6, "Payload length:", payloadLength);
39
+ const payloadOffset = headerBuf.readUInt32LE(8);
40
+ meta.payloadOffset = payloadOffset;
41
+ await _log(6, "Payload offset:", payloadOffset);
42
+ const blockSize = headerBuf.readUInt32LE(12);
43
+ meta.blockSize = blockSize;
44
+ await _log(2, "Block size:", blockSize);
45
+ if (options.crc) {
46
+ const { computedCrc, storedCrc } = await getFileCrc(fd);
47
+ const validCrc = computedCrc === storedCrc || storedCrc === 0;
48
+ await _log(2, "CRC:", computedCrc, "Needed CRC:", storedCrc, "Valid:", validCrc);
49
+ if (storedCrc === 0) {
50
+ await _log(1, "Warning: CRC is zero, CRC will not be checked");
51
+ }
52
+ if (!validCrc) {
53
+ await _log(0, "err", "Invalid CRC");
54
+ if (options.crc === 2)
55
+ throw new Error("Invalid CRC");
56
+ }
57
+ }
58
+ if (payloadOffset + payloadLength > fileSize - HEADER_SIZE) {
59
+ await _log(6, "err", "Invalid payload length");
60
+ throw new Error("Invalid payload length");
61
+ }
62
+ if (payloadLength === 0) {
63
+ await _log(6, "Empty payload, initializing collections and freeList");
64
+ return meta;
65
+ }
66
+ await readHeaderPayload(cmp);
67
+ return meta;
68
+ }
69
+ export async function readHeaderPayload(cmp) {
70
+ const { fd, meta } = cmp;
71
+ const { payloadLength, payloadOffset } = meta;
72
+ const payloadBuf = Buffer.alloc(payloadLength);
73
+ const { bytesRead } = await fd.read(payloadBuf, 0, payloadLength, HEADER_SIZE + payloadOffset);
74
+ await _log(6, `Payload header read, bytesRead: ${bytesRead}`);
75
+ if (bytesRead < payloadLength) {
76
+ await _log(6, "err", `Incomplete payload header read: expected ${payloadLength} bytes, got ${bytesRead}`);
77
+ throw new Error(`Incomplete payload header read: expected ${payloadLength} bytes, got ${bytesRead}`);
78
+ }
79
+ const obj = await cmp.options.format.decode(payloadBuf);
80
+ meta.collections = (obj.c || []).map(([name, offset, capacity]) => ({ name, offset, capacity }));
81
+ meta.freeList = (obj.f || []).map(([offset, capacity]) => ({ offset, capacity }));
82
+ await _log(6, "Collections and freeList loaded", meta);
83
+ }
84
+ export function getHeaderPayload(meta) {
85
+ return {
86
+ c: meta.collections.map(({ name, offset, capacity }) => ([name, offset, capacity])),
87
+ f: meta.freeList.map(({ offset, capacity }) => [offset, capacity]),
88
+ };
89
+ }
90
+ export async function saveHeaderAndPayload(cmp, recursion = false) {
91
+ const { fd, meta, options } = cmp;
92
+ if (!fd)
93
+ throw new Error("File not open");
94
+ const { collections, freeList, fileSize } = meta;
95
+ await _log(6, "Saving header payload:", collections, freeList);
96
+ const payloadObj = getHeaderPayload(meta);
97
+ const payloadBuf = Buffer.from(await cmp.options.format.encode(payloadObj));
98
+ if (payloadBuf.length > 64 * 1024) {
99
+ console.error("Header payload too large");
100
+ throw new Error("Header payload too large");
101
+ }
102
+ await _log(6, "Header payload length:", payloadBuf.length);
103
+ const headerBuf = Buffer.alloc(HEADER_SIZE);
104
+ headerBuf.writeUInt32LE(VERSION, 0);
105
+ headerBuf.writeUInt32LE(payloadBuf.length, 4);
106
+ headerBuf.writeUInt32LE(meta.payloadOffset, 8);
107
+ headerBuf.writeUInt32LE(meta.blockSize, 12);
108
+ meta.payloadLength = payloadBuf.length;
109
+ if (options.crc) {
110
+ const { computedCrc: crc } = await getFileCrc(fd);
111
+ headerBuf.writeUInt32LE(crc, 16);
112
+ }
113
+ await _log(6, "Writing header:", headerBuf.toString("hex"));
114
+ // Write header
115
+ await fd.write(headerBuf, 0, HEADER_SIZE, 0);
116
+ // Write payload
117
+ const roundPayload = roundUpCapacity(meta, payloadBuf.length);
118
+ if (detectCollisions(meta, HEADER_SIZE + meta.payloadOffset, roundPayload)) {
119
+ await _log(2, "Collision detected");
120
+ const slot = !recursion && await findFreeSlot(cmp, roundPayload);
121
+ if (slot) {
122
+ meta.payloadOffset = slot.offset - HEADER_SIZE;
123
+ }
124
+ else {
125
+ meta.payloadOffset = meta.fileSize - HEADER_SIZE;
126
+ meta.fileSize += roundPayload;
127
+ }
128
+ pushToFreeList(meta, meta.payloadOffset, roundPayload);
129
+ return await saveHeaderAndPayload(cmp, true);
130
+ }
131
+ await writeData(fd, HEADER_SIZE + meta.payloadOffset, payloadBuf, roundPayload);
132
+ await _log(6, "Payload written");
133
+ // Update file size if header + payload bigger
134
+ meta.fileSize = Math.max(fileSize, HEADER_SIZE + roundPayload);
135
+ }
@@ -0,0 +1,41 @@
1
+ import { FileHandle } from "fs/promises";
2
+ import { FileMeta } from "./head.js";
3
+ export interface CollectionMeta {
4
+ name: string;
5
+ offset: number;
6
+ capacity: number;
7
+ }
8
+ export interface Options {
9
+ preferredSize: number;
10
+ /**
11
+ * 0 - crc off
12
+ * 1 - warn if error
13
+ * 2 - throw if error
14
+ */
15
+ crc: number;
16
+ overwriteRemovedCollection: boolean;
17
+ format: {
18
+ encode(data: any): Promise<Parameters<typeof Buffer.from>[0]>;
19
+ decode(data: Buffer): Promise<any>;
20
+ };
21
+ }
22
+ export declare class BinManager {
23
+ path: string;
24
+ fd: null | FileHandle;
25
+ meta: FileMeta;
26
+ options: Options;
27
+ /**
28
+ * Constructs a new BinManager instance.
29
+ * @param path - File path.
30
+ * @param [preferredSize=512] - The preferred block size for the database. Must be a positive number (preferredSize > 0)
31
+ * @throws If the path is not provided, or the preferred size is
32
+ * not a positive number.
33
+ */
34
+ constructor(path: string, options?: Partial<Options>);
35
+ open(): Promise<void>;
36
+ close(): Promise<void>;
37
+ write(collection: string, data: object[]): Promise<void>;
38
+ read(collection: string): Promise<any>;
39
+ optimize(): Promise<void>;
40
+ removeCollection(collection: string): Promise<void>;
41
+ }
@@ -0,0 +1,87 @@
1
+ import * as msgpack from "@msgpack/msgpack";
2
+ import { access, constants, open } from "fs/promises";
3
+ import { getFileCrc } from "../crc32.js";
4
+ import { _log } from "../log.js";
5
+ import { readLogic, writeLogic } from "./data.js";
6
+ import { openFile } from "./head.js";
7
+ import { optimize } from "./optimize.js";
8
+ import { removeCollection } from "./rm.js";
9
+ async function safeOpen(path) {
10
+ try {
11
+ await access(path, constants.F_OK);
12
+ return await open(path, "r+");
13
+ }
14
+ catch {
15
+ _log(1, "Creating new file");
16
+ return await open(path, "w+");
17
+ }
18
+ }
19
+ export class BinManager {
20
+ path;
21
+ fd = null;
22
+ meta;
23
+ options;
24
+ /**
25
+ * Constructs a new BinManager instance.
26
+ * @param path - File path.
27
+ * @param [preferredSize=512] - The preferred block size for the database. Must be a positive number (preferredSize > 0)
28
+ * @throws If the path is not provided, or the preferred size is
29
+ * not a positive number.
30
+ */
31
+ constructor(path, options) {
32
+ this.path = path;
33
+ if (!path)
34
+ throw new Error("Path not provided");
35
+ this.options = {
36
+ preferredSize: 512,
37
+ crc: 2,
38
+ overwriteRemovedCollection: false,
39
+ format: {
40
+ encode: async (data) => msgpack.encode(data),
41
+ decode: async (data) => msgpack.decode(data)
42
+ },
43
+ ...options
44
+ };
45
+ if (!this.options.preferredSize || this.options.preferredSize <= 0)
46
+ throw new Error("Preferred size not provided");
47
+ }
48
+ async open() {
49
+ this.fd = await safeOpen(this.path);
50
+ await openFile(this);
51
+ }
52
+ async close() {
53
+ if (this.fd) {
54
+ const buff = Buffer.alloc(8);
55
+ if (this.options.crc) {
56
+ const { computedCrc: crc } = await getFileCrc(this.fd);
57
+ buff.writeUInt32LE(crc, 0);
58
+ }
59
+ else {
60
+ buff.fill(0, 0, 8);
61
+ }
62
+ await this.fd.write(buff, 0, 8, 16);
63
+ await this.fd.close();
64
+ this.fd = null;
65
+ }
66
+ }
67
+ async write(collection, data) {
68
+ if (!this.fd)
69
+ throw new Error("File not open");
70
+ await writeLogic(this, collection, data);
71
+ }
72
+ async read(collection) {
73
+ if (!this.fd)
74
+ throw new Error("File not open");
75
+ return await readLogic(this, collection);
76
+ }
77
+ async optimize() {
78
+ if (!this.fd)
79
+ throw new Error("File not open");
80
+ await optimize(this);
81
+ }
82
+ async removeCollection(collection) {
83
+ if (!this.fd)
84
+ throw new Error("File not open");
85
+ await removeCollection(this, collection);
86
+ }
87
+ }
@@ -0,0 +1,2 @@
1
+ import { BinManager } from "./index.js";
2
+ export declare function optimize(cmp: BinManager): Promise<void>;