{"id":26975158,"url":"https://github.com/izure1/serializable-bptree","last_synced_at":"2025-07-22T22:33:56.769Z","repository":{"id":207248386,"uuid":"718790888","full_name":"izure1/serializable-bptree","owner":"izure1","description":"This is a B+tree that's totally okay with duplicate values. If you need to keep track of the B+ tree's state, don't just leave it in memory - make sure you store it down.","archived":false,"fork":false,"pushed_at":"2025-06-28T17:56:18.000Z","size":153,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-07-04T01:01:53.462Z","etag":null,"topics":["bplustree","btree","javascript","json","serializable-objects","serialization","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/izure1.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-11-14T20:04:40.000Z","updated_at":"2025-06-28T17:56:21.000Z","dependencies_parsed_at":"2023-11-21T14:46:20.517Z","dependency_job_id":"26d0abe1-b887-4c7c-b300-5622e98dd58a","html_url":"https://github.com/izure1/serializable-bptree","commit_stats":{"total_commits":48,"total_committers":1,"mean_commits":48.0,"dds":0.0,"last_synced_commit":"7bdf9f5026a679c1006ea08ecd9f3c4a5df31682"},"previous_names":["izure1/serializable-bptree"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/izure1/serializable-bptree","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izure1%2Fserializable-bptree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izure1%2Fserializable-bptree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izure1%2Fserializable-bptree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izure1%2Fserializable-bptree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/izure1","download_url":"https://codeload.github.com/izure1/serializable-bptree/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izure1%2Fserializable-bptree/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266586173,"owners_count":23952170,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bplustree","btree","javascript","json","serializable-objects","serialization","typescript"],"created_at":"2025-04-03T11:19:20.056Z","updated_at":"2025-07-22T22:33:56.756Z","avatar_url":"https://github.com/izure1.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# serializable-bptree\r\n\r\n[![](https://data.jsdelivr.com/v1/package/npm/serializable-bptree/badge)](https://www.jsdelivr.com/package/npm/serializable-bptree)\r\n![Node.js workflow](https://github.com/izure1/serializable-bptree/actions/workflows/node.js.yml/badge.svg)\r\n\r\nThis is a B+tree that's totally okay with duplicate values. If you need to keep track of the B+ tree's state, don't just leave it in memory - make sure you write it down.\r\n\r\n```typescript\r\nimport { readFileSync, writeFileSync, unlinkSync, existsSync } from 'fs'\r\nimport {\r\n  BPTreeSync,\r\n  SerializeStrategySync,\r\n  NumericComparator\r\n} from 'serializable-bptree'\r\n\r\nclass FileStoreStrategySync extends SerializeStrategySync\u003cK, V\u003e {\r\n  id(): string {\r\n    return this.autoIncrement('index', 1).toString()\r\n  }\r\n\r\n  read(id: string): BPTreeNode\u003cK, V\u003e {\r\n    const raw = readFileSync(id, 'utf8')\r\n    return JSON.parse(raw)\r\n  }\r\n\r\n  write(id: string, node: BPTreeNode\u003cK, V\u003e): void {\r\n    const stringify = JSON.stringify(node)\r\n    writeFileSync(id, stringify, 'utf8')\r\n  }\r\n\r\n  delete(id: string): void {\r\n    unlinkSync(id)\r\n  }\r\n\r\n  readHead(): SerializeStrategyHead|null {\r\n    if (!existsSync('head')) {\r\n      return null\r\n    }\r\n    const raw = readFileSync('head', 'utf8')\r\n    return JSON.parse(raw)\r\n  }\r\n\r\n  writeHead(head: SerializeStrategyHead): void {\r\n    const stringify = JSON.stringify(head)\r\n    writeFileSync('head', stringify, 'utf8')\r\n  }\r\n}\r\n\r\nconst order = 5\r\nconst tree = new BPTreeSync(\r\n  new FileStoreStrategySync(order),\r\n  new NumericComparator()\r\n)\r\n\r\ntree.init()\r\ntree.insert('a', 1)\r\ntree.insert('b', 2)\r\ntree.insert('c', 3)\r\n\r\ntree.delete('b', 2)\r\n\r\ntree.where({ equal: 1 }) // Map([{ key: 'a', value: 1 }])\r\ntree.where({ gt: 1 }) // Map([{ key: 'c', value: 3 }])\r\ntree.where({ lt: 2 }) // Map([{ key: 'a', value: 1 }])\r\ntree.where({ gt: 0, lt: 4 }) // Map([{ key: 'a', value: 1 }, { key: 'c', value: 3 }])\r\ntree.where({ or: [3, 1] }) // Map([{ key: 'a', value: 1 }, { key: 'c', value: 3 }])\r\n\r\ntree.clear()\r\n```\r\n\r\n## Why use a `serializable-bptree`?\r\n\r\nFirstly, in most cases, there is no need to use a B+tree in JavaScript. This is because there is a great alternative, the Map object. Nonetheless, if you need to retrieve values in a sorted order, a B+tree can be a good solution. These cases are often related to databases, and you may want to store this state not just in memory, but on a remote server or in a file. In this case, **serializable-bptree** can help you.\r\n\r\nAdditionally, this library supports asynchronous operations. Please refer to the section below for instructions on using it asynchronously.\r\n\r\n## How to use\r\n\r\n### Node.js (cjs)\r\n\r\n```bash\r\nnpm i serializable-bptree\r\n```\r\n\r\n```typescript\r\nimport {\r\n  BPTreeSync,\r\n  BPTreeAsync,\r\n  SerializeStrategySync,\r\n  SerializeStrategyAsync,\r\n  NumericComparator,\r\n  StringComparator\r\n} from 'serializable-bptree'\r\n```\r\n\r\n### Browser (esm)\r\n\r\n```html\r\n\u003cscript type=\"module\"\u003e\r\n  import {\r\n    BPTreeSync,\r\n    BPTreeAsync,\r\n    InMemoryStoreStrategySync,\r\n    InMemoryStoreStrategyAsync,\r\n    ValueComparator,\r\n    NumericComparator,\r\n    StringComparator\r\n  } from 'https://cdn.jsdelivr.net/npm/serializable-bptree@5/+esm'\r\n\u003c/script\u003e\r\n```\r\n\r\n## Conceptualization\r\n\r\n### Value comparator\r\n\r\nB+tree needs to keep values in sorted order. Therefore, a process to compare the sizes of values is needed, and that role is played by the **ValueComparator**.\r\n\r\nCommonly used numerical and string comparisons are natively supported by the **serializable-bptree** library. Use it as follows:\r\n\r\n```typescript\r\nimport { NumericComparator, StringComparator } from 'serializable-bptree'\r\n```\r\n\r\nHowever, you may want to sort complex objects other than numbers and strings. For example, if you want to sort by the **age** property order of an object, you need to create a new class that inherits from the **ValueComparator** class. Use it as follows:\r\n\r\n```typescript\r\nimport { ValueComparator } from 'serializable-bptree'\r\n\r\ninterface MyObject {\r\n  age: number\r\n  name: string\r\n}\r\n\r\nclass AgeComparator extends ValueComparator\u003cMyObject\u003e {\r\n  asc(a: MyObject, b: MyObject): number {\r\n    return a.age - b.age\r\n  }\r\n\r\n  match(value: MyObject): string {\r\n    return value.age.toString()\r\n  }\r\n}\r\n```\r\n\r\n#### asc\r\n\r\nThe **asc** method should return values in ascending order. If the return value is negative, it means that the parameter **a** is smaller than **b**. If the return value is positive, it means that **a** is greater than **b**. If the return value is **0**, it indicates that **a** and **b** are of the same size.\r\n\r\n#### match\r\n\r\nThe `match` method is used for the **LIKE** operator. This method specifies which value to test against a regular expression. For example, if you have a tree with values of the structure `{ country: string, capital: string }`, and you want to perform a **LIKE** operation based on the **capital** value, the method should return **value.capital**. In this case, you **CANNOT** perform a **LIKE** operation based on the **country** attribute. The returned value must be a string.\r\n\r\n```typescript\r\ninterface MyObject {\r\n  country: string\r\n  capital: string\r\n}\r\n\r\nclass CompositeComparator extends ValueComparator\u003cMyObject\u003e {\r\n  ...\r\n  match(value: MyObject): string {\r\n    return value.capital\r\n  }\r\n}\r\n```\r\n\r\nFor a tree with simple structure, without complex nesting, returning the value directly would be sufficient.\r\n\r\n```typescript\r\nclass StringComparator extends ValueComparator\u003cstring\u003e {\r\n  match(value: string): string {\r\n    return value\r\n  }\r\n}\r\n```\r\n\r\n### Serialize strategy\r\n\r\nA B+tree instance is made up of numerous nodes. You would want to store this value when such nodes are created or updated. Let's assume you want to save it to a file.\r\n\r\nYou need to construct a logic for input/output from the file by inheriting the SerializeStrategy class. Look at the class structure below:\r\n\r\n```typescript\r\nimport { SerializeStrategySync } from 'serializable-bptree'\r\n\r\nclass MyFileIOStrategySync extends SerializeStrategySync {\r\n  id(): string\r\n  read(id: string): BPTreeNode\u003cK, V\u003e\r\n  write(id: string, node: BPTreeNode\u003cK, V\u003e): void\r\n  delete(id: string): void\r\n  readHead(): SerializeStrategyHead|null\r\n  writeHead(head: SerializeStrategyHead): void\r\n}\r\n```\r\n\r\nWhat does this method mean? And why do we need to construct such a method?\r\n\r\n#### id(isLeaf: `boolean`): `string`\r\n\r\nWhen a node is created in the B+tree, the node needs a unique value to represent itself. This is the **node.id** attribute, and you can specify this attribute yourself.\r\n\r\nTypically, such an **id** value increases sequentially, and it would be beneficial to store such a value separately within the tree. For that purpose, the **setHeadData** and **getHeadData** methods are available. These methods are responsible for storing arbitrary data in the tree's header or retrieving stored data. Below is an example of usage:\r\n\r\n```typescript\r\nid(isLeaf: boolean): string {\r\n  const current = this.getHeadData('index', 1) as number\r\n  this.setHeadData('index', current+1)\r\n  return current.toString()\r\n}\r\n```\r\n\r\nAdditionally, there is a more dev-friendly usage of this code.\r\n\r\n```typescript\r\nid(isLeaf: boolean): string {\r\n  return this.autoIncrement('index', 1).toString()\r\n}\r\n```\r\n\r\nThe **id** method is called before a node is created in the tree. Therefore, it can also be used to allocate space for storing the node.\r\n\r\n#### read(id: `string`): `BPTreeNode\u003cK, V\u003e`\r\n\r\nThis is a method to load the saved value as a tree instance. If you have previously saved the node as a file, you should use this method to convert it back to JavaScript JSON format and return it.\r\n\r\nPlease refer to the example below:\r\n\r\n```typescript\r\nread(id: string): BPTreeNode\u003cK, V\u003e {\r\n  const filePath = `./my-store/${id}`\r\n  const raw = fs.readFileSync(filePath, 'utf8')\r\n  return JSON.parse(raw)\r\n}\r\n```\r\n\r\nThis method is called only once when loading a node from a tree instance. The loaded node is loaded into memory, and subsequently, when the tree references the node, it operates based on the values in memory **without** re-invoking this method.\r\n\r\n#### write(id: `string`, node: `BPTreeNode\u003cK, V\u003e`): `void`\r\n\r\nThis method is called when there are changes in the internal nodes due to the insert or delete operations of the tree instance. In other words, it's a necessary method for synchronizing the in-memory nodes into a file.\r\n\r\nSince this method is called frequently, be mindful of performance. There are ways to optimize it using a write-back caching technique.\r\n\r\nPlease refer to the example below:\r\n\r\n```typescript\r\nlet queue = 0\r\nfunction writeBack(id: string, node: BPTreeNode\u003cK, V\u003e, timer: number) {\r\n  clearTimeout(queue)\r\n  queue = setTimeout(() =\u003e {\r\n    const filePath = `./my-store/${id}`\r\n    const stringify = JSON.stringify(node)\r\n    writeFileSync(filePath, stringify, 'utf8')\r\n  }, timer)\r\n}\r\n\r\n...\r\nwrite(id: string, node: BPTreeNode\u003cK, V\u003e): void {\r\n  const writeBackInterval = 10\r\n  writeBack(id, node, writeBackInterval)\r\n}\r\n```\r\n\r\nThis kind of delay writing should ideally occur within a few milliseconds. If this is not feasible, consider other approaches.\r\n\r\n#### delete(id: `string`): `void`\r\n\r\nThis method is called when previously created nodes become no longer needed due to deletion or other processes. It can be used to free up space by deleting existing stored nodes.\r\n\r\n```typescript\r\ndelete(id: string): void {\r\n  const filePath = `./my-store/${id}`\r\n  fs.unlinkSync(filePath)\r\n}\r\n```\r\n\r\n#### readHead(): `SerializeStrategyHead`|`null`\r\n\r\nThis method is called only once when the tree is created. It's a method to restore the saved tree information. If it is the initial creation and there is no stored root node, it should return **null**.\r\n\r\nThis method should return the value stored in the **writeHead** method.\r\n\r\n#### writeHead(head: `SerializeStrategyHead`): `void`\r\n\r\nThis method is called whenever the head information of the tree changes, typically when the root node changes. This method also works when the tree's **setHeadData** method is called. This is because the method attempts to store head data in the root node.\r\n\r\nAs a parameter, it receives the header information of the tree. This value should be serialized and stored. Later, the **readHead** method should convert this serialized value into a json format and return it.\r\n\r\n### The Default `ValueComparator` and `SerializeStrategy`\r\n\r\nTo utilize **serializable-bptree**, you need to implement certain functions. However, a few basic helper classes are provided by default.\r\n\r\n#### ValueComparator\r\n\r\n* `NumericComparator`\r\n* `StringComparator`\r\n\r\nIf the values being inserted into the tree are numeric, please use the **NumericComparator** class.\r\n\r\n```typescript\r\nimport { NumericComparator } from 'serializable-bptree'\r\n```\r\n\r\nIf the values being inserted into the tree can be strings, you can use the **StringComparator** class in this case.\r\n\r\n```typescript\r\nimport { StringComparator } from 'serializable-bptree'\r\n```\r\n\r\n#### SerializeStrategy\r\n\r\n* `InMemoryStoreStrategySync`\r\n* `InMemoryStoreStrategyAsync`\r\n\r\nAs of now, the only class supported by default is the **InMemoryStoreStrategy**. This class is suitable for use when you prefer to operate the tree solely in-memory, similar to a typical B+ tree.\r\n\r\n```typescript\r\nimport {\r\n  InMemoryStoreStrategySync,\r\n  InMemoryStoreStrategyAsync\r\n} from 'serializable-bptree'\r\n```\r\n\r\n## Data Query Condition Clause\r\n\r\nThis library supports various conditional clauses. Currently, it supports **gte**, **gt**, **lte**, **lt**, **equal**, **notEqual**, **or**, and **like** conditions. Each condition is as follows:\r\n\r\n### `gte`\r\n\r\nQueries values that are greater than or equal to the given value.\r\n\r\n```typescript\r\ntree.where({ gte: 1 })\r\n```\r\n\r\n### `gt`\r\n\r\nQueries values that are greater than the given value.\r\n\r\n```typescript\r\ntree.where({ gt: 1 })\r\n```\r\n\r\n### `lte`\r\n\r\nQueries values that are less than or equal to the given value.\r\n\r\n```typescript\r\ntree.where({ lte: 5 })\r\n```\r\n\r\n### `lt`\r\n\r\nQueries values that are less than the given value.\r\n\r\n```typescript\r\ntree.where({ lt: 5 })\r\n```\r\n\r\n### `equal`\r\n\r\nQueries values that match the given value.\r\n\r\n```typescript\r\ntree.where({ equal: 3 })\r\n```\r\n\r\n### `notEqual`\r\n\r\nQueries values that do not match the given value.\r\n\r\n```typescript\r\ntree.where({ notEqual: 3 })\r\n```\r\n\r\n### `or`\r\n\r\nQueries values that satisfy at least one of the given conditions. It accepts an array of conditions, and if any of these conditions are met, the data is included in the result.\r\n\r\n```typescript\r\ntree.where({ or:  [1, 2, 3] })\r\n```\r\n\r\n### `like`\r\n\r\nQueries values that contain the given value in a manner similar to regular expressions. Special characters such as % and _ can be used.\r\n\r\n**%** matches zero or more characters. For example, **%ada%** means all strings that contain \"ada\" anywhere in the string. **%ada** means strings that end with \"ada\". **ada%** means strings that start with **\"ada\"**.\r\n\r\n**_** matches exactly one character.\r\nUsing **p_t**, it can match any string where the underscore is replaced by any character, such as \"pit\", \"put\", etc.\r\n\r\nYou can obtain matching data by combining these condition clauses. If there are multiple conditions, an **AND** operation is used to retrieve only the data that satisfies all conditions.\r\n\r\n```typescript\r\ntree.where({ like: 'hello%' })\r\ntree.where({ like: 'he__o%' })\r\ntree.where({ like: '%world!' })\r\ntree.where({ like: '%lo, wor%' })\r\n```\r\n\r\n## Using Asynchronously\r\n\r\nSupport for asynchronous trees has been available since version 3.0.0. Asynchronous is useful for operations with delays, such as file input/output and remote storage. Here is an example of how to use it:\r\n\r\n```typescript\r\nimport { existsSync } from 'fs'\r\nimport { readFile, writeFile, unlink } from 'fs/promises'\r\nimport {\r\n  BPTreeAsync,\r\n  SerializeStrategyAsync,\r\n  NumericComparator,\r\n  StringComparator\r\n} from 'serializable-bptree'\r\n\r\nclass FileStoreStrategyAsync extends SerializeStrategyAsync\u003cK, V\u003e {\r\n  async id(isLeaf: boolean): Promise\u003cstring\u003e {\r\n    return await this.autoIncrement('index', 1).toString()\r\n  }\r\n\r\n  async read(id: string): Promise\u003cBPTreeNode\u003cK, V\u003e\u003e {\r\n    const raw = await readFile(id, 'utf8')\r\n    return JSON.parse(raw)\r\n  }\r\n\r\n  async write(id: string, node: BPTreeNode\u003cK, V\u003e): Promise\u003cvoid\u003e {\r\n    const stringify = JSON.stringify(node)\r\n    await writeFile(id, stringify, 'utf8')\r\n  }\r\n\r\n  async delete(id: string): Promise\u003cvoid\u003e {\r\n    await unlink(id)\r\n  }\r\n\r\n  async readHead(): Promise\u003cSerializeStrategyHead|null\u003e {\r\n    if (!existsSync('head')) {\r\n      return null\r\n    }\r\n    const raw = await readFile('head', 'utf8')\r\n    return JSON.parse(raw)\r\n  }\r\n\r\n  async writeHead(head: SerializeStrategyHead): Promise\u003cvoid\u003e {\r\n    const stringify = JSON.stringify(head)\r\n    await writeFile('head', stringify, 'utf8')\r\n  }\r\n}\r\n\r\nconst order = 5\r\nconst tree = new BPTreeAsync(\r\n  new FileStoreStrategyAsync(order),\r\n  new NumericComparator()\r\n)\r\n\r\nawait tree.init()\r\nawait tree.insert('a', 1)\r\nawait tree.insert('b', 2)\r\nawait tree.insert('c', 3)\r\n\r\nawait tree.delete('b', 2)\r\n\r\nawait tree.where({ equal: 1 }) // Map([{ key: 'a', value: 1 }])\r\nawait tree.where({ gt: 1 }) // Map([{ key: 'c', value: 3 }])\r\nawait tree.where({ lt: 2 }) // Map([{ key: 'a', value: 1 }])\r\nawait tree.where({ gt: 0, lt: 4 }) // Map([{ key: 'a', value: 1 }, { key: 'c', value: 3 }])\r\n\r\ntree.clear()\r\n```\r\n\r\nThe implementation method for asynchronous operations is not significantly different. The **-Async** suffix is used instead of the **-Sync** suffix in the **BPTree** and **SerializeStrategy** classes. The only difference is that the methods become asynchronous. The **ValueComparator** class and similar value comparators do not use asynchronous operations.\r\n\r\n## Precautions for Use\r\n\r\n### Synchronization Issue\r\n\r\nThe serializable-bptree minimizes file I/O by storing loaded nodes in-memory. This approach works well in situations where there is a 1:1 relationship between the remote storage and the client. However, in a 1:n scenario, where multiple clients read from and write to a single remote storage, data inconsistency between the remote storage and the clients can occur.\r\n\r\nTo solve this problem, it's necessary to update the cached nodes. The forceUpdate method was created for this purpose. It fetches the node data cached in the tree instance again. To use this feature, when you save data to the remote storage, you must send a signal to all clients connected to that remote storage indicating that the node has been updated. Clients must receive this signal and configure logic to call the **forceUpdate** method; however, this goes beyond the scope of the library, so you must implement it yourself.\r\n\r\n### Concurrency Issue in Asynchronous Trees\r\n\r\nThis issue occurs only in asynchronous trees and can also occur in a 1:1 relationship between remote storage and client. During the process of inserting/removing data asynchronously, querying the data can result in inconsistent data. To prevent concurrency issues, do not query data while inserting/removing it.\r\n\r\n## LICENSE\r\n\r\nMIT LICENSE\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizure1%2Fserializable-bptree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fizure1%2Fserializable-bptree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizure1%2Fserializable-bptree/lists"}