diff --git a/scripts/quick-db/README.md b/scripts/quick-db/README.md index c7c2e1a0..6337ec1f 100644 --- a/scripts/quick-db/README.md +++ b/scripts/quick-db/README.md @@ -1,171 +1,93 @@ -# QuickDB - A Lightweight Database for Minecraft Bedrock ScriptAPI +# QuickDB -**QuickDB** is a simple and efficient database system designed for Minecraft Bedrock Edition ScriptAPI. It utilizes dynamic properties from the `@minecraft/server` module, allowing developers to store and manage key-value pairs in a way similar to JavaScript's `Map` object. +QuickDB is a lightweight key-value database designed for Minecraft Bedrock Script API. +It supports two storage backends: ---- - -## Features - -- **CRUD Operations**: - - `set(key, value)` - Save a value to the database. - - `get(key)` - Retrieve a value by its key. - - `delete(key)` - Remove a key-value pair from the database. - - `has(key)` - Check if a key exists in the database. - -- **Iteration**: - - `keys()` - Get all keys in the database. - - `values()` - Retrieve all values stored in the database. - - `entries()` - Retrieve all key-value pairs as an array of entries. - -- **Database Size**: - - `size` - Get the total byte count used by the dynamic properties. - ---- - -## Installation - -Import `QuickDB` into your ScriptAPI project. Ensure `QuickDB` is included in your `index.js` file for easy integration. - -```javascript -import QuickDB from "./index.js"; -``` - ---- - -## Documentation - -### **Constructor** - -```javascript -const db = new QuickDB("user"); -``` - -- **Parameters**: - - `id` *(string)*: A unique identifier for your database instance. - ---- +- **Dynamic Properties** +- **Scoreboard** -### **Methods** - -#### `set(key, value)` -- **Description**: Stores a value associated with a key. -- **Parameters**: - - `key` *(string)*: The key to store the value. - - `value` *(any)*: The value to be stored. -- **Returns**: `boolean` - `true` if successful. - -```javascript -db.set("FomoKiwor", { money: 20000 }); -``` +Both storages share the same API, so you can switch storage types without changing your code. --- -#### `get(key)` -- **Description**: Retrieves the value associated with a key. -- **Parameters**: - - `key` *(string)*: The key to retrieve the value. -- **Returns**: `any` - The value associated with the key. - -```javascript -const userData = db.get("FomoKiwor"); // { money: 20000 } -``` - ---- +# How Caching Works -#### `has(key)` -- **Description**: Checks if a key exists in the database. -- **Parameters**: - - `key` *(string)*: The key to check. -- **Returns**: `boolean` - `true` if the key exists. +When a database is created, it **loads all existing data into memory** (RAM). -```javascript -const exists = db.has("FomoKiwor"); // true -``` +Example process: ---- +1. The database scans the storage (Scoreboard or Dynamic Property). +2. All matching keys are loaded into an internal cache. +3. Future operations read from this cache instead of scanning storage again. -#### `delete(key)` -- **Description**: Removes a key-value pair from the database. -- **Parameters**: - - `key` *(string)*: The key to remove. -- **Returns**: `boolean` - `true` if successful. +Because of this: -```javascript -db.delete("FomoKiwor"); -``` +- `get()` is **very fast** +- `has()` is **very fast** +- `keys()`, `values()`, `entries()` do not need to scan storage again ---- +When data changes: -#### `keys()` -- **Description**: Retrieves all keys in the database. -- **Returns**: `string[]` - An array of all keys. +- `set()` updates **storage + cache** +- `delete()` removes **storage + cache** -```javascript -const allKeys = db.keys(); // ["key1", "key2"] -``` +This keeps both storage and cache synchronized. --- -#### `values()` -- **Description**: Retrieves all values in the database. -- **Returns**: `any[]` - An array of all values. +# Storage Types -```javascript -const allValues = db.values(); // [{ money: 20000 }, { items: [] }] -``` - ---- +| Storage Type | Backend Used | Description | +| ------------ | ------------ | ------------------------------------ | +| `local` | DynamicDB | Stores data using dynamic properties | +| `dynamic` | DynamicDB | Same as local | +| `global` | ScoreboardDB | Stores data using scoreboard | +| `scoreboard` | ScoreboardDB | Same as global | -#### `entries()` -- **Description**: Retrieves all key-value pairs in the database. -- **Returns**: `Array<[string, any]>` - An array of key-value entries. +Example: -```javascript -const allEntries = db.entries(); // [["key1", { money: 20000 }], ["key2", { items: [] }]] +```ts +const db = new QuickDB("playerData"); // default using local or dynamic +const globalDB = new QuickDB("playerData", "global"); // global database, different cache with local or dynamic database, use this to sync database with other addons you wanna make like plugin structures ``` --- -### **Property** +# Methods -#### `size` -- **Description**: Gets the total byte count used by dynamic properties. -- **Returns**: `number` - The total byte count. - -```javascript -console.log(db.size); // e.g., 512 -``` +| Method | Description | Example | +| ----------------- | ------------------------ | ---------------------- | +| `set(key, value)` | Save or update a value | `db.set("coins", 100)` | +| `get(key)` | Get value from cache | `db.get("coins")` | +| `has(key)` | Check if key exists | `db.has("coins")` | +| `delete(key)` | Remove key from database | `db.delete("coins")` | +| `keys()` | Get all keys | `db.keys()` | +| `values()` | Get all values | `db.values()` | +| `entries()` | Get key-value pairs | `db.entries()` | --- -## Example Usage - -```javascript -import QuickDB from "./index.js"; +# Example Usage -const db = new QuickDB("user"); +```ts +const db = new QuickDB("coins"); -db.set("FomoKiwor", { money: 20000 }); -console.log(db.get("FomoKiwor")); // { money: 20000 } +db.set("player1", 100); -console.log(db.has("FomoKiwor")); // true -db.delete("FomoKiwor"); -console.log(db.has("FomoKiwor")); // false +const coins = db.get("player1"); -db.set("User1", { score: 100 }); -db.set("User2", { score: 150 }); - -console.log(db.keys()); // ["User1", "User2"] -console.log(db.values()); // [{ score: 100 }, { score: 150 }] -console.log(db.entries()); // [["User1", { score: 100 }], ["User2", { score: 150 }]] +console.log(coins); ``` --- -## License +# Summary -MIT License +QuickDB works by: ---- +1. Loading data from storage +2. Storing it in an internal cache +3. Using the cache for fast access +4. Keeping storage and cache synchronized -Developed by [Nperma](https://github.com/nperma) +This makes database operations **fast and efficient** for Minecraft Script API projects. diff --git a/scripts/quick-db/index.js b/scripts/quick-db/index.js index a123782b..35652f02 100644 --- a/scripts/quick-db/index.js +++ b/scripts/quick-db/index.js @@ -1,135 +1,138 @@ -// Script example for ScriptAPI -// Author: Nperma -// Project: https://github.com/JaylyDev/ScriptAPI - -import { world,system, World } from '@minecraft/server'; - -const DATABASE_PREFIX = '\u0235\u0235'; - -const { - getDynamicProperty: GET, - setDynamicProperty: SET, - getDynamicPropertyIds: IDS -} = World.prototype; - -class QuickDB { - #identifier; - __cache = {}; - - /** - * @param {string} id - Unique database identifier. - */ - constructor(id) { - if (typeof id !== 'string' || !id.trim()) { - throw new Error('Invalid database ID'); - } - this.#identifier = `${DATABASE_PREFIX}${id}${DATABASE_PREFIX}`; - - for (const keyFull of this.getIds()) { - const key = keyFull.replace(this.#identifier, ''); - let value;system.run(()=>{value=GET.call(world, keyFull);}) - this.__cache[key] = JSON.parse(value); - } - } - - /** @returns {number} */ - get size() { - return this.keys().length; - } - - /** @returns {string[]} */ - keys() { - return Object.keys(this.__cache); - } - - /** @returns {any[]} */ - values() { - return Object.values(this.__cache); - } - - /** @returns {[string, any][]} */ - entries() { - return Object.entries(this.__cache); - } - - /** - * Stores a key-value pair. - * @param {string} key - * @param {any} value - * @returns {void} - */ - set(key, value) { - if (typeof key !== 'string' || !key.trim()) throw new Error('Key must be a non-empty string'); - system.run(()=>SET.call(world, this.#identifier + key, JSON.stringify(value))); - this.__cache[key] = value; - } - - /** - * Deletes a key. - * @param {string} key - * @returns {boolean} - */ - delete(key) { - if (!this.has(key)) return false; - system.run(()=>SET.call(world, this.#identifier + key)); - delete this.__cache[key]; - return true; - } - - /** - * Retrieves a value. - * @param {string} key - * @returns {any} - */ - get(key) { - if (typeof key !== 'string' || !key.trim()) throw new Error('Key must be a non-empty string'); - return this.__cache[key]; - } - - /** - * Checks if a key exists. - * @param {string} key - * @returns {boolean} - */ - has(key) { - return key in this.__cache; - } - - /** @returns {string[]} */ - static get ids() { - let keys; - system.run(() =>{ - keys=IDS.call(world) - .filter((id) => id.startsWith(DATABASE_PREFIX)) - .map((k) => k.slice(DATABASE_PREFIX.length).split(DATABASE_PREFIX)[0]); - }); - return [...new Set( - keys - )]; - } - - /** @returns {string[]} */ - getIds() { - let result;system.run(()=>{result=IDS.call(world).filter((id) => id.startsWith(this.#identifier));}); - return result; - } - - /** Clears the database. */ - clear() { - for (const key of this.keys()) { - this.delete(key); - } - this.__cache = {}; - } - - /** Clears all databases globally. */ - static clearAll() { - let keys;system.run(()=>{keys=IDS.call(world).filter((id) => id.startsWith(DATABASE_PREFIX))}); - for (const real_id of keys) { - system.run(()=>SET.call(world, real_id)); - } - } +import { world } from "@minecraft/server"; +const KEY_4NO = "⧉⧉"; +export class ScoreboardDB { + constructor(name) { + this.keyacc = '⟡⟡'; + this.cache = {}; + this.identifier = `${KEY_4NO}${name}${KEY_4NO}`; + this.obj = world.scoreboard.getObjective(this.identifier) ?? + world.scoreboard.addObjective(this.identifier); + this.participantNames = {}; + this.participants = this.obj.getParticipants(); + let iteration = this.participants.length; + while (iteration--) { + const participantName = this.participants[iteration]?.displayName; + const [key, rawValue] = participantName.split(this.keyacc); + const value = rawValue.startsWith('num:') ? Number(rawValue.slice('num:'.length)) : rawValue.startsWith('bool:') ? Boolean(rawValue.slice('bool:'.length)) : JSON.parse(rawValue); + this.participantNames[key] = participantName; + this.cache[key] = value; + } + } + set(key, value) { + if (key in this.cache && value == null) + this.delete(key); + const valueFormatting = typeof value == 'number' ? `num:${value}` : typeof value == 'boolean' ? `bool:${value}` : value; + const participantName = `${key}${this.keyacc}${JSON.stringify(valueFormatting)}`; + this.obj.setScore(participantName, 1); + this.participantNames[key] = participantName; + this.cache[key] = value; + } + get(key) { + return this.cache[key]; + } + has(key) { + return !!this.cache; + } + delete(key) { + if (!(key in this.cache)) + return false; + this.obj.removeParticipant(this.participantNames[key]); + delete this.participantNames[key]; + delete this.cache[key]; + return true; + } + keys() { + return Object.keys(this.cache); + } + values() { + return Object.values(this.cache); + } + entries() { + return Object.entries(this.cache); + } +} +export class DynamicDB { + constructor(name) { + this.cache = {}; + this.identifier = `${KEY_4NO}${name}${KEY_4NO}`; + const ids = world.getDynamicPropertyIds(); + let i = ids.length; + while (i--) { + const id = ids[i]; + if (!id.startsWith(this.identifier)) + continue; + const raw = world.getDynamicProperty(id); + const key = id.slice(this.identifier.length); + const value = typeof raw === "string" + ? JSON.parse(raw) + : raw; + this.cache[key] = value; + } + } + set(key, value) { + const id = this.identifier + key; + if (key in this.cache && value == null) { + this.delete(key); + return; + } + const data = typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ? value + : JSON.stringify(value); + world.setDynamicProperty(id, data); + this.cache[key] = value; + } + get(key) { + return this.cache[key]; + } + has(key) { + return key in this.cache; + } + delete(key) { + if (!(key in this.cache)) + return false; + world.setDynamicProperty(this.identifier + key, undefined); + delete this.cache[key]; + return true; + } + keys() { + return Object.keys(this.cache); + } + values() { + return Object.values(this.cache); + } + entries() { + return Object.entries(this.cache); + } +} +DynamicDB.size = world.getDynamicPropertyTotalByteCount(); +export default class QuickDB { + constructor(name, storage = "local") { + this.db = + storage === "scoreboard" || storage === "global" + ? new ScoreboardDB(name) + : new DynamicDB(name); + } + set(key, value) { + this.db.set(key, value); + } + get(key) { + return this.db.get(key); + } + has(key) { + return this.db.has(key); + } + delete(key) { + return this.db.delete(key); + } + keys() { + return this.db.keys(); + } + values() { + return this.db.values(); + } + entries() { + return this.db.entries(); + } } - -export default QuickDB; -export { QuickDB };