diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4dc150e9f8..0a45925390 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -20,6 +20,7 @@ /packages/result/**/*.ts @kyranet @vladfrangu @favna /packages/snowflake/**/*.ts @favna @vladfrangu @kyranet /packages/stopwatch/**/*.ts @kyranet @vladfrangu +/packages/string-store/**/*.ts @kyranet /packages/time-utilities/**/*.ts @favna @kyranet @vladfrangu /packages/ts-config/**/*.ts @favna /packages/utilities/**/*.ts @favna @kyranet @vladfrangu diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index af8dd73df4..767749b262 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -46,6 +46,7 @@ jobs: - result - snowflake - stopwatch + - string-store - time-utilities - timer-manager - timestamp diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index ae1c740746..0d5eac37a1 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -72,6 +72,7 @@ jobs: - result - snowflake - stopwatch + - string-store - time-utilities - timer-manager - timestamp diff --git a/.npm-deprecaterc.yml b/.npm-deprecaterc.yml index dc8257e4fd..9bb18664b1 100644 --- a/.npm-deprecaterc.yml +++ b/.npm-deprecaterc.yml @@ -20,6 +20,7 @@ package: - '@sapphire/result' - '@sapphire/snowflake' - '@sapphire/stopwatch' + - '@sapphire/string-store' - '@sapphire/time-utilities' - '@sapphire/timer-manager' - '@sapphire/timestamp' diff --git a/README.md b/README.md index 1250ef87a3..301e7769e8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ [![npm](https://img.shields.io/npm/v/@sapphire/result?color=crimson&logo=npm&style=flat-square&label=@sapphire/result)](https://www.npmjs.com/package/@sapphire/result) [![npm](https://img.shields.io/npm/v/@sapphire/snowflake?color=crimson&logo=npm&style=flat-square&label=@sapphire/snowflake)](https://www.npmjs.com/package/@sapphire/snowflake) [![npm](https://img.shields.io/npm/v/@sapphire/stopwatch?color=crimson&logo=npm&style=flat-square&label=@sapphire/stopwatch)](https://www.npmjs.com/package/@sapphire/stopwatch) +[![npm](https://img.shields.io/npm/v/@sapphire/string-store?color=crimson&logo=npm&style=flat-square&label=@sapphire/string-store)](https://www.npmjs.com/package/@sapphire/string-store) [![npm](https://img.shields.io/npm/v/@sapphire/time-utilities?color=crimson&logo=npm&style=flat-square&label=@sapphire/time-utilities)](https://www.npmjs.com/package/@sapphire/time-utilities) [![npm](https://img.shields.io/npm/v/@sapphire/timer-manager?color=crimson&logo=npm&style=flat-square&label=@sapphire/timer-manager)](https://www.npmjs.com/package/@sapphire/timer-manager) [![npm](https://img.shields.io/npm/v/@sapphire/timestamp?color=crimson&logo=npm&style=flat-square&label=@sapphire/timestamp)](https://www.npmjs.com/package/@sapphire/timestamp) diff --git a/packages/string-store/.cliff-jumperrc.yml b/packages/string-store/.cliff-jumperrc.yml new file mode 100644 index 0000000000..61d32a388e --- /dev/null +++ b/packages/string-store/.cliff-jumperrc.yml @@ -0,0 +1,9 @@ +name: string-store +org: sapphire +install: true +packagePath: packages/string-store +identifierBase: false +pushTag: true +githubRelease: true +githubReleaseLatest: true +githubRepo: sapphiredev/utilities diff --git a/packages/string-store/.typedoc-json-parserrc.yml b/packages/string-store/.typedoc-json-parserrc.yml new file mode 100644 index 0000000000..92d0e01d7e --- /dev/null +++ b/packages/string-store/.typedoc-json-parserrc.yml @@ -0,0 +1 @@ +json: 'docs/api.json' diff --git a/packages/string-store/README.md b/packages/string-store/README.md new file mode 100644 index 0000000000..0dc9ece90b --- /dev/null +++ b/packages/string-store/README.md @@ -0,0 +1,284 @@ +
+ +![Sapphire Logo](https://raw.githubusercontent.com/sapphiredev/assets/main/banners/SapphireCommunity.png) + +# @sapphire/string-store + +**High-capacity raw data storage in UTF-16 strings.** + +[![GitHub](https://img.shields.io/github/license/sapphiredev/utilities)](https://github.com/sapphiredev/utilities/blob/main/LICENSE.md) +[![codecov](https://codecov.io/gh/sapphiredev/utilities/branch/main/graph/badge.svg?token=OEGIV6RFDO)](https://codecov.io/gh/sapphiredev/utilities) +[![npm bundle size](https://img.shields.io/bundlephobia/min/@sapphire/string-store?logo=webpack&style=flat-square)](https://bundlephobia.com/result?p=@sapphire/string-store) +[![npm](https://img.shields.io/npm/v/@sapphire/string-store?color=crimson&logo=npm&style=flat-square)](https://www.npmjs.com/package/@sapphire/string-store) + +
+ +## Description + +A package that can store large chunks of data in a short UTF-16 string, useful for storing data in length-limited +locations such as Discord's `custom_id` field in message components. + +## Features + +- Written in TypeScript +- Bundled with esbuild so it can be used in NodeJS and browsers +- Offers CommonJS, ESM and UMD bundles +- Fully tested + +## Installation + +You can use the following command to install this package, or replace `npm install` with your package manager of choice. + +```sh +npm install @sapphire/string-store +``` + +## Usage + +**Note**: While this section uses `require`, the imports match 1:1 with ESM imports. For example, `const { SchemaStore } = require('@sapphire/string-store')` is equivalent to `import { SchemaStore } from '@sapphire/string-store'`. + +```ts +// Require the store classes +const { SchemaStore, Schema } = require('@sapphire/string-store'); + +const Id = { + AgeUpdate: 0, + StrengthUpdate: 1, + Planet: 2, + User: 3 +}; + +// Create the store +const store = new SchemaStore() + // Add a schema with an age field stored as a int32: + // Schema + .add(new Schema(Id.AgeUpdate).int32('age')) + // Add a schema with a strength field stored as a float32: + // Schema + .add(new Schema(Id.StrengthUpdate).float32('strength')); + +// Serialize an `Id.AgeUpdate` object into a string containing: +// - The schema ID (0) +// - The age field (20) +const buffer = store.serialize(Id.AgeUpdate, { age: 20 }).toString(); +``` + +> [!Tip] +> The serialized string is encoded in UTF-16, meaning it can store 16 bits per character. Each type stores a different number of bits, for example, a single character can store: +> - 16 booleans +> - 8 2-bit integers (0-3) +> - 4 4-bit integers (0-15) +> - 2 8-bit integers (0-255) +> - 1 16-bit integer (0-65535) +> +> As a use-case, Discord's `custom_id` field in message components can store up to **100** UTF-16 characters, which means it has a storage of **1600 bits**, below you can see the supported types and their storage in bits. Keep in mind that the schema ID is stored as a [16-bit](#int16) integer, and that the property names are **not** stored. + +The schema can be defined using the following methods: + +### `array` + +Adds an array with a dynamic length to the schema. + +```ts +// A schema with a single field `names` that is an array of strings: + +const schema = new Schema(Id.Planets).array('names', StringType); +// → Schema +``` + +To track the length of the array, it will serialize a [16-bit](#int16) unsigned integer before the array. + +### `fixedLengthArray` + +An alternative to [`array`](#array) that has a fixed length, will require the exact number of elements to be serialized, +but it will save space by not storing the length of the array. + +```ts +// A schema with a single field `names` that is an array of exactly 3 strings: + +const schema = new Schema(Id.Planets).fixedLengthArray('names', StringType, 3); +// → Schema +``` + +### `string` + +Adds a string to the schema. + +```ts +// A schema with a single field `name` that is a string: + +const schema = new Schema(Id.Planet).string('name'); +// → Schema +``` + +The string is serialized as UTF-8, and the length is serialized as a [16-bit](#int16) unsigned integer before the string. + +### `boolean` + +Adds a boolean (single bit) to the schema. + +```ts +// A schema with a single field `isHabitable` that is a boolean: + +const schema = new Schema(Id.Planet).boolean('isHabitable'); +// → Schema +``` + +### `bit` + +Adds a bit (0 or 1) to the schema. This is a numeric version of [`boolean`](#boolean). + +```ts +// A schema with a single field `isHabitable` that is a bit: + +const schema = new Schema(Id.Planet).bit('isHabitable'); +// → Schema +``` + +### `int2` + +Adds a 2-bit integer to the schema. It can store values from 0 to 3 (`0b11`), inclusive. + +```ts +// A schema with a single field `type` that is a 2-bit integer: + +const schema = new Schema(Id.Planet).int2('type'); +// → Schema +``` + +### `int4` + +Adds a 4-bit integer to the schema. It can store values from 0 to 15 (`0b1111`), inclusive. + +```ts +// A schema with a single field `type` that is a 4-bit integer: + +const schema = new Schema(Id.Planet).int4('type'); +// → Schema +``` + +### `int8` + +Adds an 8-bit integer to the schema. It can store values from 0 to 255 (`0b1111_1111`), inclusive. + +```ts +// A schema with a single field `type` that is an 8-bit integer: + +const schema = new Schema(Id.Planet).int8('type'); +// → Schema +``` + +### `int16` + +Adds a 16-bit integer to the schema. It can store values from 0 to 65535 (`0xFFFF`), inclusive. + +```ts +// A schema with a single field `type` that is a 16-bit integer: + +const schema = new Schema(Id.Planet).int16('type'); +// → Schema +``` + +### `int32` + +Adds a 32-bit integer to the schema. It can store values from 0 to 4294967295 (`0xFFFFFFFF`), inclusive. + +```ts +// A schema with a single field `type` that is a 32-bit integer: + +const schema = new Schema(Id.Planet).int32('type'); +// → Schema +``` + +### `int64` + +Adds a 64-bit integer to the schema. It can store values from 0 to 9007199254740991 (`Number.MAX_SAFE_INTEGER`), inclusive. + +```ts +// A schema with a single field `type` that is a 64-bit integer: + +const schema = new Schema(Id.Planet).int64('type'); +// → Schema +``` + +**Note**: values larger than `Number.MAX_SAFE_INTEGER` will be truncated. + +### `bigInt32` + +Alternative to [`int32`](#int32) that uses `BigInt`. + +```ts +// A schema with a single field `type` that is a 32-bit integer: + +const schema = new Schema(Id.Planet).bigInt32('type'); +// → Schema +``` + +### `bigInt64` + +Alternative to [`int64`](#int64) that uses `BigInt`. + +```ts +// A schema with a single field `type` that is a 64-bit integer: + +const schema = new Schema(Id.Planet).bigInt64('type'); +// → Schema +``` + +### `float32` + +Adds a 32-bit floating-point number to the schema. + +```ts +// A schema with a single field `radius` that is a 32-bit floating-point number: + +const schema = new Schema(Id.Planet).float32('radius'); +// → Schema +``` + +### `float64` + +Adds a 64-bit floating-point number to the schema. + +```ts +// A schema with a single field `radius` that is a 64-bit floating-point number: + +const schema = new Schema(Id.Planet).float64('radius'); +// → Schema +``` + +### `snowflake` + +Adds a 64-bit snowflake to the schema. + +```ts +const schema = new Schema(Id.User).snowflake('id'); +// → Schema +``` + +--- + +## Buy us some doughnuts + +Sapphire Community is and always will be open source, even if we don't get donations. That being said, we know there are amazing people who may still want to donate just to show their appreciation. Thank you very much in advance! + +We accept donations through Open Collective, Ko-fi, PayPal, Patreon and GitHub Sponsorships. You can use the buttons below to donate through your method of choice. + +| Donate With | Address | +| :-------------: | :-------------------------------------------------: | +| Open Collective | [Click Here](https://sapphirejs.dev/opencollective) | +| Ko-fi | [Click Here](https://sapphirejs.dev/kofi) | +| Patreon | [Click Here](https://sapphirejs.dev/patreon) | +| PayPal | [Click Here](https://sapphirejs.dev/paypal) | + +## Contributors + +Please make sure to read the [Contributing Guide][contributing] before making a pull request. + +Thank you to all the people who already contributed to Sapphire! + + + + + +[contributing]: https://github.com/sapphiredev/.github/blob/main/.github/CONTRIBUTING.md diff --git a/packages/string-store/cliff.toml b/packages/string-store/cliff.toml new file mode 100644 index 0000000000..bb184e0969 --- /dev/null +++ b/packages/string-store/cliff.toml @@ -0,0 +1,77 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file.\n +""" +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +{% if version %}\ + # [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + ({{ self::remote_url() }}/tree/{{ version }})\ + {% endif %}\ + {% endif %} \ + - ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %}\ + # [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.pr_number %} (\ + [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) by @{{ commit.github.username }}) \ + {%- endif %}\ + {% if commit.breaking %}\ + {% for breakingChange in commit.footers %}\ + \n{% raw %} {% endraw %}- 💥 **{{ breakingChange.token }}{{ breakingChange.separator }}** {{ breakingChange.value }}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} +{% endfor %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "🚀 Features" }, + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^docs", group = "📝 Documentation" }, + { message = "^perf", group = "🏃 Performance" }, + { message = "^refactor", group = "🏠 Refactor" }, + { message = "^typings", group = "⌨️ Typings" }, + { message = "^types", group = "⌨️ Typings" }, + { message = ".*deprecated", body = ".*deprecated", group = "🚨 Deprecation" }, + { message = "^revert", skip = true }, + { message = "^style", group = "🪞 Styling" }, + { message = "^test", group = "🧪 Testing" }, + { message = "^chore", skip = true }, + { message = "^ci", skip = true }, + { message = "^build", skip = true }, + { body = ".*security", group = "🛡️ Security" }, +] +commit_preprocessors = [ + # remove issue numbers from commits + { pattern = '\s\((\w+\s)?#([0-9]+)\)', replace = "" }, +] +filter_commits = true +tag_pattern = "@sapphire/string-store@[0-9]*" +ignore_tags = "" +topo_order = false +sort_commits = "newest" + +[remote.github] +owner = "sapphiredev" +repo = "utilities" diff --git a/packages/string-store/package.json b/packages/string-store/package.json new file mode 100644 index 0000000000..e4b863eecc --- /dev/null +++ b/packages/string-store/package.json @@ -0,0 +1,74 @@ +{ + "name": "@sapphire/string-store", + "version": "1.0.0", + "description": "High-capacity raw data storage in UTF-16 strings", + "author": "@sapphire", + "license": "MIT", + "main": "dist/cjs/index.cjs", + "module": "dist/esm/index.mjs", + "browser": "dist/iife/index.global.js", + "unpkg": "dist/iife/index.global.js", + "types": "dist/cjs/index.d.cts", + "exports": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.cts", + "default": "./dist/cjs/index.cjs" + }, + "browser": "./dist/iife/index.global.js" + }, + "sideEffects": false, + "homepage": "https://github.com/sapphiredev/utilities/tree/main/packages/string-store", + "scripts": { + "test": "vitest run", + "lint": "eslint src tests --ext ts --fix -c ../../.eslintrc", + "docs": "typedoc-json-parser", + "build": "yarn gen-index && tsup && yarn build:rename-cjs-index", + "build:rename-cjs-index": "tsx ../../scripts/rename-cjs-index.cts", + "prepack": "yarn build", + "bump": "cliff-jumper", + "check-update": "cliff-jumper --dry-run", + "gen-index": "tsx ../../scripts/gen-index.cts -w string-store" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/sapphiredev/utilities.git", + "directory": "packages/string-store" + }, + "files": [ + "dist/" + ], + "engines": { + "node": ">=v20", + "npm": ">=10" + }, + "keywords": [ + "@sapphire/string-store", + "bot", + "typescript", + "ts", + "yarn", + "discord", + "sapphire", + "standalone" + ], + "bugs": { + "url": "https://github.com/sapphiredev/utilities/issues" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@favware/cliff-jumper": "^4.0.2", + "@vitest/coverage-v8": "^2.0.2", + "tsup": "^8.1.0", + "tsx": "^4.16.2", + "typedoc": "^0.25.13", + "typedoc-json-parser": "^10.0.0", + "typescript": "~5.4.5", + "vitest": "^2.0.2" + } +} diff --git a/packages/string-store/src/index.ts b/packages/string-store/src/index.ts new file mode 100644 index 0000000000..06f60e5a5d --- /dev/null +++ b/packages/string-store/src/index.ts @@ -0,0 +1,21 @@ +export * from './lib/schema/Schema'; +export * from './lib/schema/SchemaStore'; +export * from './lib/shared/Pointer'; +export * from './lib/types/Array'; +export type * from './lib/types/base/IType'; +export * from './lib/types/BigInt32'; +export * from './lib/types/BigInt64'; +export * from './lib/types/Bit'; +export * from './lib/types/Boolean'; +export * from './lib/types/FixedLengthArray'; +export * from './lib/types/Float32'; +export * from './lib/types/Float64'; +export * from './lib/types/Int16'; +export * from './lib/types/Int2'; +export * from './lib/types/Int32'; +export * from './lib/types/Int4'; +export * from './lib/types/Int64'; +export * from './lib/types/Int8'; +export * from './lib/types/Snowflake'; +export * from './lib/types/String'; +export * from './lib/UnalignedUint16Array'; diff --git a/packages/string-store/src/lib/UnalignedUint16Array.ts b/packages/string-store/src/lib/UnalignedUint16Array.ts new file mode 100644 index 0000000000..5d6c09612a --- /dev/null +++ b/packages/string-store/src/lib/UnalignedUint16Array.ts @@ -0,0 +1,236 @@ +import { Pointer, type PointerLike } from './shared/Pointer'; + +const Converter8 = new Uint8Array(8); +const ConverterFloat = new Float32Array(Converter8.buffer); +const ConverterDouble = new Float64Array(Converter8.buffer); + +export class UnalignedUint16Array { + #buffer: Uint16Array; + #bitLength = 0; + #wordIndex = 0; + #wordLength = 0; + + public constructor(maxLength: number) { + this.#buffer = new Uint16Array(maxLength); + } + + public get maxLength(): number { + return this.#buffer.length; + } + + public get maxBitLength(): number { + return this.#buffer.length * 16; + } + + public get length(): number { + return this.#wordLength; + } + + public get bitLength(): number { + return this.#bitLength; + } + + public writeBit(value: number): void { + this.#writeBit(value); + } + + public writeInt2(value: number): void { + this.writeBit(value & 1); + this.writeBit(value >> 1); + } + + public writeInt4(value: number): void { + this.writeInt2(value & 0b11); + this.writeInt2(value >> 2); + } + + public writeInt8(value: number): void { + this.writeInt4(value & 0b1111); + this.writeInt4(value >> 4); + } + + public writeInt16(value: number): void { + this.writeInt8(value & 0xff); + this.writeInt8(value >> 8); + } + + public writeInt32(value: number): void { + this.writeInt16(value & 0xffff); + this.writeInt16(value >> 16); + } + + public writeInt64(value: number): void { + this.writeBigInt64(BigInt(value)); + } + + public writeBigInt32(value: bigint): void { + this.writeInt16(Number(value & 0xffffn)); + this.writeInt16(Number(value >> 16n)); + } + + public writeBigInt64(value: bigint): void { + this.writeInt32(Number(value & 0xffffffffn)); + this.writeInt32(Number(value >> 32n)); + } + + public writeFloat32(value: number): void { + ConverterFloat[0] = value; + this.writeInt8(Converter8[0]); + this.writeInt8(Converter8[1]); + this.writeInt8(Converter8[2]); + this.writeInt8(Converter8[3]); + } + + public writeFloat64(value: number): void { + ConverterDouble[0] = value; + this.writeInt8(Converter8[0]); + this.writeInt8(Converter8[1]); + this.writeInt8(Converter8[2]); + this.writeInt8(Converter8[3]); + this.writeInt8(Converter8[4]); + this.writeInt8(Converter8[5]); + this.writeInt8(Converter8[6]); + this.writeInt8(Converter8[7]); + } + + public readBit(offset: PointerLike): 0 | 1 { + const ptr = Pointer.from(offset); + return this.#readBit(ptr) as 0 | 1; + } + + public readInt2(offset: PointerLike): number { + const ptr = Pointer.from(offset); + return this.#readBit(ptr) | (this.#readBit(ptr) << 1); + } + + public readInt4(offset: PointerLike): number { + const ptr = Pointer.from(offset); + return this.#readBit(ptr) | (this.#readBit(ptr) << 1) | (this.#readBit(ptr) << 2) | (this.#readBit(ptr) << 3); + } + + public readInt8(offset: PointerLike): number { + const ptr = Pointer.from(offset); + return this.#readByte(ptr); + } + + public readInt16(offset: PointerLike): number { + const ptr = Pointer.from(offset); + return this.#readByte(ptr) | (this.#readByte(ptr) << 8); + } + + public readInt32(offset: PointerLike): number { + return Number(this.readBigInt32(offset)); + } + + public readInt64(offset: PointerLike) { + return Number(this.readBigInt64(offset)); + } + + public readBigInt32(offset: PointerLike): bigint { + const ptr = Pointer.from(offset); + return ( + BigInt(this.#readByte(ptr)) | + (BigInt(this.#readByte(ptr)) << 8n) | + (BigInt(this.#readByte(ptr)) << 16n) | + (BigInt(this.#readByte(ptr)) << 24n) + ); + } + + public readBigInt64(offset: PointerLike): bigint { + const ptr = Pointer.from(offset); + return ( + BigInt(this.#readByte(ptr)) | + (BigInt(this.#readByte(ptr)) << 8n) | + (BigInt(this.#readByte(ptr)) << 16n) | + (BigInt(this.#readByte(ptr)) << 24n) | + (BigInt(this.#readByte(ptr)) << 32n) | + (BigInt(this.#readByte(ptr)) << 40n) | + (BigInt(this.#readByte(ptr)) << 48n) | + (BigInt(this.#readByte(ptr)) << 56n) + ); + } + + public readFloat32(offset: PointerLike): number { + const ptr = Pointer.from(offset); + Converter8[0] = this.#readByte(ptr); + Converter8[1] = this.#readByte(ptr); + Converter8[2] = this.#readByte(ptr); + Converter8[3] = this.#readByte(ptr); + return ConverterFloat[0]; + } + + public readFloat64(offset: PointerLike): number { + const ptr = Pointer.from(offset); + Converter8[0] = this.#readByte(ptr); + Converter8[1] = this.#readByte(ptr); + Converter8[2] = this.#readByte(ptr); + Converter8[3] = this.#readByte(ptr); + Converter8[4] = this.#readByte(ptr); + Converter8[5] = this.#readByte(ptr); + Converter8[6] = this.#readByte(ptr); + Converter8[7] = this.#readByte(ptr); + return ConverterDouble[0]; + } + + public toString() { + let result = ''; + for (let i = 0; i < this.length; i++) { + result += String.fromCharCode(this.#buffer[i]); + } + + return result; + } + + public toArray(): Uint16Array { + return this.#buffer.slice(0, this.length); + } + + #readBit(pointer: Pointer) { + const bitOffset = pointer.value; + const index = bitOffset >> 4; + const bitIndex = bitOffset & 0xf; + pointer.add(1); + return (this.#buffer[index] >> bitIndex) & 1; + } + + #readByte(ptr: Pointer) { + return ( + this.#readBit(ptr) | + (this.#readBit(ptr) << 1) | + (this.#readBit(ptr) << 2) | + (this.#readBit(ptr) << 3) | + (this.#readBit(ptr) << 4) | + (this.#readBit(ptr) << 5) | + (this.#readBit(ptr) << 6) | + (this.#readBit(ptr) << 7) + ); + } + + #writeBit(value: number) { + if (this.#wordIndex === this.maxLength) { + throw new RangeError(`The buffer is full`); + } + + if (value) { + const index = this.#wordIndex; + const bitIndex = this.bitLength & 0xf; + this.#buffer[index] |= 1 << bitIndex; + } + + if ((this.#bitLength & 0xf) === 0) this.#wordLength++; + this.#bitLength++; + if ((this.#bitLength & 0xf) === 0) this.#wordIndex++; + } + + public static from(value: string | UnalignedUint16Array): UnalignedUint16Array { + if (value instanceof UnalignedUint16Array) return value; + + const buffer = new UnalignedUint16Array(value.length); + for (let i = 0; i < value.length; i++) { + buffer.#buffer[i] = value.charCodeAt(i); + } + + buffer.#bitLength = value.length << 4; + return buffer; + } +} diff --git a/packages/string-store/src/lib/schema/Schema.ts b/packages/string-store/src/lib/schema/Schema.ts new file mode 100644 index 0000000000..82b3f2f950 --- /dev/null +++ b/packages/string-store/src/lib/schema/Schema.ts @@ -0,0 +1,355 @@ +import { Pointer, type PointerLike } from '../shared/Pointer'; +import { ArrayType } from '../types/Array'; +import type { IType } from '../types/base/IType'; +import { BigInt32Type } from '../types/BigInt32'; +import { BigInt64Type } from '../types/BigInt64'; +import { BitType } from '../types/Bit'; +import { BooleanType } from '../types/Boolean'; +import { FixedLengthArrayType } from '../types/FixedLengthArray'; +import { Float32Type } from '../types/Float32'; +import { Float64Type } from '../types/Float64'; +import { Int16Type } from '../types/Int16'; +import { Int2Type } from '../types/Int2'; +import { Int32Type } from '../types/Int32'; +import { Int4Type } from '../types/Int4'; +import { Int64Type } from '../types/Int64'; +import { Int8Type } from '../types/Int8'; +import { SnowflakeType } from '../types/Snowflake'; +import { StringType } from '../types/String'; +import type { UnalignedUint16Array } from '../UnalignedUint16Array'; + +export class Schema { + readonly #id: Id; + readonly #types = new Map>(); + #bitSize: number | null = 0; + + /** + * Creates a new schema. + * + * @param id The id of the schema + */ + public constructor(id: Id) { + this.#id = id; + } + + /** + * The id of the schema. + */ + public get id(): Id { + return this.#id; + } + + /** + * The total bit size of the schema. + * + * @remarks + * + * If any of the entries have a bit size of `null`, the bit size of the + * schema will also be `null`. + */ + public get bitSize(): number | null { + return this.#bitSize; + } + + /** + * Get a property from the schema. + * + * @param name The name of the property + * @returns The specified property + * + * @remarks + * + * If the property does not exist, an error will be thrown. + */ + public get(name: Name): Entries[Name] { + const type = this.#types.get(name) as Entries[Name]; + if (!type) throw new Error(`Schema with id ${this.#id} does not have a property with name "${name}"`); + return type; + } + + /** + * Serialize a value into a buffer. + * + * @param buffer The buffer to serialize + * @param value The value to serialize into the buffer + * + * @remarks + * + * The schema's ID is written to the buffer first, followed by each property + * in the schema. + */ + public serialize(buffer: UnalignedUint16Array, value: Readonly>): void { + buffer.writeInt16(this.#id); + for (const [name, type] of this) { + (type as IType).serialize(buffer, (value as any)[name]); + } + } + + /** + * Deserialize a value from a buffer. + * + * @param buffer The buffer to deserialize + * @param pointer The pointer to where the buffer should be read from + * @returns The deserialized value + * + * @remarks + * + * Unlike {@link Schema.serialize}, this method does not read the schema's ID + * from the buffer, that is reserved for the {@link SchemaStore}. + */ + public deserialize(buffer: UnalignedUint16Array, pointer: PointerLike): UnwrapSchemaEntries { + const ptr = Pointer.from(pointer); + const result = Object.create(null) as UnwrapSchemaEntries; + for (const [name, type] of this) { + // @ts-expect-error Complex types + result[name] = type.deserialize(buffer, ptr); + } + return result; + } + + /** + * Adds an array property to the schema. + * + * @seealso {@link Schema.fixedLengthArray} for a fixed length array + * + * @param name The name of the property + * @param type The type of the entry in the array + * @returns The modified schema + */ + public array( + name: Name, + type: IType + ) { + return this.#addType(name, ArrayType(type)); + } + + /** + * Adds a fixed length array property to the schema. + * + * @seealso {@link Schema.array} for a dynamic length array + * + * @param name The name of the property + * @param type The type of the entry in the array + * @param length The length of the array + * @returns The modified schema + */ + public fixedLengthArray( + name: Name, + type: IType, + length: number + ) { + return this.#addType(name, FixedLengthArrayType(type, length)); + } + + /** + * Adds a string property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public string(name: Name) { + return this.#addType(name, StringType); + } + + /** + * Adds a boolean property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public boolean(name: Name) { + return this.#addType(name, BooleanType); + } + + /** + * Adds a bit property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public bit(name: Name) { + return this.#addType(name, BitType); + } + + /** + * Adds a 2-bit integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public int2(name: Name) { + return this.#addType(name, Int2Type); + } + + /** + * Adds a 4-bit integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public int4(name: Name) { + return this.#addType(name, Int4Type); + } + + /** + * Adds a 8-bit integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public int8(name: Name) { + return this.#addType(name, Int8Type); + } + + /** + * Adds a 16-bit integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public int16(name: Name) { + return this.#addType(name, Int16Type); + } + + /** + * Adds a 32-bit integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public int32(name: Name) { + return this.#addType(name, Int32Type); + } + + /** + * Adds a 64-bit integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public int64(name: Name) { + return this.#addType(name, Int64Type); + } + + /** + * Adds a 32-bit big integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public bigInt32(name: Name) { + return this.#addType(name, BigInt32Type); + } + + /** + * Adds a 64-bit big integer property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public bigInt64(name: Name) { + return this.#addType(name, BigInt64Type); + } + + /** + * Adds a 32-bit floating point number property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public float32(name: Name) { + return this.#addType(name, Float32Type); + } + + /** + * Adds a 64-bit floating point number property to the schema. + * + * @param name The name of the property + * @returns The modified schema + */ + public float64(name: Name) { + return this.#addType(name, Float64Type); + } + + /** + * Adds a 64-bit big integer property to the schema, similar to {@link Schema.bigInt64}. + * + * @param name The name of the property + * @returns The modified schema + */ + public snowflake(name: Name) { + return this.#addType(name, SnowflakeType); + } + + /** + * Iterates over the schema's property names. + * + * @returns An iterator for the schema's property names + */ + public keys(): IterableIterator> { + return this.#types.keys() as IterableIterator>; + } + + /** + * Iterates over the schema's property values + * + * @returns An iterator for the schema's property values + */ + public values(): IterableIterator> { + return this.#types.values() as IterableIterator>; + } + + /** + * Iterates over the schema's property entries + * + * @returns An iterator for the schema's property entries + */ + public entries(): IterableIterator> { + return this.#types.entries() as IterableIterator>; + } + + /** + * Iterates over the schema's property entries + * + * @returns An iterator for the schema's property entries + */ + public [Symbol.iterator](): IterableIterator> { + return this.entries(); + } + + #addType( + name: EntryName, + type: IType + ): Merge { + if (this.#types.has(name)) { + throw new Error(`Schema with id ${this.#id} already has a property with name "${name}"`); + } + + this.#types.set(name, type); + + if (type.BIT_SIZE === null) { + this.#bitSize = null; + } else if (this.#bitSize !== null) { + this.#bitSize += type.BIT_SIZE; + } + + return this as unknown as Merge; + } +} + +type Merge< + Id extends number, + Entries extends object, + EntryName extends string, + EntryType extends IType +> = EntryName extends keyof Entries ? never : Schema; + +export type KeyOfSchema = SchemaValue extends Schema ? keyof Type & string : never; +export type ValueOfSchema = + SchemaValue extends Schema ? { [K in keyof Type]: Type[K] }[keyof Type] : never; +export type EntryOfSchema = + SchemaValue extends Schema ? { [K in keyof Type]: readonly [K, Type[K]] }[keyof Type] : never; + +export type UnwrapSchemaType = Type extends IType ? T : never; +export type UnwrapSchemaEntries = { [K in keyof Entries]: UnwrapSchemaType } & object; +export type UnwrapSchema = SchemaValue extends Schema ? UnwrapSchemaEntries : never; diff --git a/packages/string-store/src/lib/schema/SchemaStore.ts b/packages/string-store/src/lib/schema/SchemaStore.ts new file mode 100644 index 0000000000..9420f6e8f9 --- /dev/null +++ b/packages/string-store/src/lib/schema/SchemaStore.ts @@ -0,0 +1,134 @@ +import { Pointer } from '../shared/Pointer'; +import { UnalignedUint16Array } from '../UnalignedUint16Array'; +import { Schema, type UnwrapSchema } from './Schema'; + +export class SchemaStore { + /** + * The default maximum array length for schemas + */ + public defaultMaximumArrayLength: number; + + #schemas = new Map(); + + /** + * Creates a new schema store + * + * @param defaultMaximumArrayLength The default maximum array length for schemas + */ + public constructor(defaultMaximumArrayLength = 100) { + this.defaultMaximumArrayLength = defaultMaximumArrayLength; + } + + /** + * Adds a schema to the store + * + * @param schema The schema to add to the store + * @returns The modified store + * + * @remarks + * + * An error will be thrown if a schema with the same id already exists in the store. + */ + public add(schema: Schema): Merge { + if (this.#schemas.has(schema.id)) { + throw new Error(`Schema with id ${schema.id} already exists`); + } + + this.#schemas.set(schema.id, schema as any); + return this as unknown as Merge; + } + + /** + * Gets a schema from the store + * + * @param id The id of the schema to get + * @returns The schema with the given id + * + * @remarks + * + * An error will be thrown if a schema with the given id does not exist in the store. + */ + public get>(id: Id): Entries[Id] { + const schema = this.#schemas.get(id) as Entries[Id]; + if (!schema) throw new Error(`Schema with id ${id} does not exist`); + return schema; + } + + /** + * Serializes a value using the schema with the given id + * + * @param id The id of the schema to use for serialization + * @param value The value to serialize + * @returns The serialized buffer + */ + public serialize>(id: Id, value: Readonly>): UnalignedUint16Array { + const schema = this.get(id) as Schema; + const buffer = new UnalignedUint16Array(schema.bitSize ?? this.defaultMaximumArrayLength); + schema.serialize(buffer, value); + return buffer; + } + + /** + * Deserializes a buffer + * + * @param buffer The buffer to deserialize + * @returns The resolved value, including the id of the schema used for deserialization + */ + public deserialize(buffer: string | UnalignedUint16Array): DeserializationResult { + buffer = UnalignedUint16Array.from(buffer); + const pointer = new Pointer(); + const id = buffer.readInt16(pointer) as KeyOfStore; + const schema = this.get(id) as Schema; + return { id, data: schema.deserialize(buffer, pointer) } as unknown as DeserializationResult; + } + + /** + * Iterates over the stores's schema identifiers. + * + * @returns An iterator for the stores's schema identifiers + */ + public keys(): IterableIterator> { + return this.#schemas.keys() as IterableIterator>; + } + + /** + * Iterates over the stores's schemas. + * + * @returns An iterator for the stores's schemas + */ + public values(): IterableIterator> { + return this.#schemas.values() as IterableIterator>; + } + + /** + * Iterates over the stores's schema entries. + * + * @returns An iterator for the stores's schema entries + */ + public entries(): IterableIterator> { + return this.#schemas.entries() as IterableIterator>; + } + + /** + * Iterates over the stores's schema entries. + * + * @returns An iterator for the stores's schema entries + */ + public [Symbol.iterator](): IterableIterator> { + return this.entries(); + } +} + +type Merge = Id extends keyof Entries + ? never + : SchemaStore<{ [K in Id | keyof Entries]: K extends keyof Entries ? Entries[K] : Type }>; + +export type KeyOfStore = SchemaStoreValue extends SchemaStore ? keyof Schemas & number : never; +export type ValueOfStore = + SchemaStoreValue extends SchemaStore ? Schemas[keyof Schemas & number] : never; +export type EntryOfStore = + SchemaStoreValue extends SchemaStore ? { [K in keyof Schemas]: readonly [K & number, Schemas[K]] }[keyof Schemas] : never; + +export type DeserializationResult = { + [K in keyof SchemaStoreEntries]: { id: K; data: UnwrapSchema }; +}[keyof SchemaStoreEntries]; diff --git a/packages/string-store/src/lib/shared/Pointer.ts b/packages/string-store/src/lib/shared/Pointer.ts new file mode 100644 index 0000000000..47529d901b --- /dev/null +++ b/packages/string-store/src/lib/shared/Pointer.ts @@ -0,0 +1,44 @@ +import { isValidLength } from './_common'; + +/** + * A pointer to a position in a buffer. + * + * This is used to keep track of the current position in a buffer while allowing + * the position to be updated by multiple different functions. + * + * @privateRemarks + * + * This class draws inspiration from the following constructs: + * - `int*` in C/C++ + * - `ref int` in C# + * - `*mut i32` in Rust + */ +export class Pointer { + #value = 0; + + public get value() { + return this.#value; + } + + public add(value: number) { + const added = this.#value + value; + if (!isValidLength(added)) { + throw new RangeError(`The pointer value cannot be an invalid length value`); + } + + this.#value = added; + return this; + } + + public static from(pointer: PointerLike) { + if (pointer instanceof Pointer) { + return pointer; + } + + const instance = new Pointer(); + instance.add(Number(pointer)); + return instance; + } +} + +export type PointerLike = Pointer | { valueOf(): number } | { [Symbol.toPrimitive](hint: 'number'): number }; diff --git a/packages/string-store/src/lib/shared/_common.ts b/packages/string-store/src/lib/shared/_common.ts new file mode 100644 index 0000000000..7b1334223d --- /dev/null +++ b/packages/string-store/src/lib/shared/_common.ts @@ -0,0 +1,24 @@ +export function isArrayLike(object: unknown): object is ArrayLike { + // If the item is not an object, it is not array-like: + if (!isObject(object)) return false; + // If the item is an array, it is array-like: + if (Array.isArray(object)) return true; + // If the item doesn't have a numeric length property, it is not array-like: + if (!hasLength(object)) return false; + // If the length isn't a valid index, it is not array-like: + if (!isValidLength(object.length)) return false; + + return object.length === 0 || object.length - 1 in object; +} + +function isObject(item: unknown): item is object { + return typeof item === 'object' && item !== null; +} + +function hasLength(item: object): item is { length: number } { + return 'length' in item && typeof item.length === 'number'; +} + +export function isValidLength(length: number) { + return Number.isSafeInteger(length) && length >= 0 && length < 2147483648; +} diff --git a/packages/string-store/src/lib/types/Array.ts b/packages/string-store/src/lib/types/Array.ts new file mode 100644 index 0000000000..e4834cf80d --- /dev/null +++ b/packages/string-store/src/lib/types/Array.ts @@ -0,0 +1,26 @@ +import { isArrayLike } from '../shared/_common'; +import type { IType } from './base/IType'; + +export function ArrayType(type: IType): IType { + return { + serialize(buffer, values: readonly ValueType[]) { + if (!isArrayLike(values)) { + throw new TypeError(`Expected an array, got ${values}`); + } + + buffer.writeInt16(values.length); + for (const value of values) { + type.serialize(buffer, value); + } + }, + deserialize(buffer, pointer) { + const length = buffer.readInt16(pointer); + const value = []; + for (let i = 0; i < length; i++) { + value.push(type.deserialize(buffer, pointer)); + } + return value; + }, + BIT_SIZE: null + }; +} diff --git a/packages/string-store/src/lib/types/BigInt32.ts b/packages/string-store/src/lib/types/BigInt32.ts new file mode 100644 index 0000000000..fe0e3eb83e --- /dev/null +++ b/packages/string-store/src/lib/types/BigInt32.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const BigInt32Type: IType = { + serialize(buffer, value) { + buffer.writeBigInt32(value); + }, + deserialize(buffer, pointer) { + return buffer.readBigInt32(pointer); + }, + BIT_SIZE: 32 +}; diff --git a/packages/string-store/src/lib/types/BigInt64.ts b/packages/string-store/src/lib/types/BigInt64.ts new file mode 100644 index 0000000000..6b765b92ec --- /dev/null +++ b/packages/string-store/src/lib/types/BigInt64.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const BigInt64Type: IType = { + serialize(buffer, value) { + buffer.writeBigInt64(value); + }, + deserialize(buffer, pointer) { + return buffer.readBigInt64(pointer); + }, + BIT_SIZE: 64 +}; diff --git a/packages/string-store/src/lib/types/Bit.ts b/packages/string-store/src/lib/types/Bit.ts new file mode 100644 index 0000000000..f79762eddd --- /dev/null +++ b/packages/string-store/src/lib/types/Bit.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const BitType: IType = { + serialize(buffer, value) { + buffer.writeBit(value & 0b1); + }, + deserialize(buffer, pointer) { + return buffer.readBit(pointer); + }, + BIT_SIZE: 1 +}; diff --git a/packages/string-store/src/lib/types/Boolean.ts b/packages/string-store/src/lib/types/Boolean.ts new file mode 100644 index 0000000000..d19a086a55 --- /dev/null +++ b/packages/string-store/src/lib/types/Boolean.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const BooleanType: IType = { + serialize(buffer, value) { + buffer.writeBit(Number(value)); + }, + deserialize(buffer, pointer) { + return buffer.readBit(pointer) === 1; + }, + BIT_SIZE: 1 +}; diff --git a/packages/string-store/src/lib/types/FixedLengthArray.ts b/packages/string-store/src/lib/types/FixedLengthArray.ts new file mode 100644 index 0000000000..ceee0a15c6 --- /dev/null +++ b/packages/string-store/src/lib/types/FixedLengthArray.ts @@ -0,0 +1,27 @@ +import { isArrayLike } from '../shared/_common'; +import type { IType } from './base/IType'; + +export function FixedLengthArrayType( + type: IType, + length: number +): IType { + return { + serialize(buffer, values) { + if (!isArrayLike(values) || values.length !== length) { + throw new TypeError(`Expected array of length ${length}, got ${values.length}`); + } + + for (let i = 0; i < length; i++) { + type.serialize(buffer, values[i]); + } + }, + deserialize(buffer, pointer) { + const value = []; + for (let i = 0; i < length; i++) { + value.push(type.deserialize(buffer, pointer)); + } + return value; + }, + BIT_SIZE: (typeof type.BIT_SIZE === 'number' ? type.BIT_SIZE * length : null) as ValueBitSize extends null ? null : number + }; +} diff --git a/packages/string-store/src/lib/types/Float32.ts b/packages/string-store/src/lib/types/Float32.ts new file mode 100644 index 0000000000..b2eac5bb6d --- /dev/null +++ b/packages/string-store/src/lib/types/Float32.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Float32Type: IType = { + serialize(buffer, value) { + buffer.writeFloat32(value); + }, + deserialize(buffer, pointer) { + return buffer.readFloat32(pointer); + }, + BIT_SIZE: 32 +}; diff --git a/packages/string-store/src/lib/types/Float64.ts b/packages/string-store/src/lib/types/Float64.ts new file mode 100644 index 0000000000..07d1553d0e --- /dev/null +++ b/packages/string-store/src/lib/types/Float64.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Float64Type: IType = { + serialize(buffer, value) { + buffer.writeFloat64(value); + }, + deserialize(buffer, pointer) { + return buffer.readFloat64(pointer); + }, + BIT_SIZE: 64 +}; diff --git a/packages/string-store/src/lib/types/Int16.ts b/packages/string-store/src/lib/types/Int16.ts new file mode 100644 index 0000000000..8ad1132102 --- /dev/null +++ b/packages/string-store/src/lib/types/Int16.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Int16Type: IType = { + serialize(buffer, value) { + buffer.writeInt16(value); + }, + deserialize(buffer, pointer) { + return buffer.readInt16(pointer); + }, + BIT_SIZE: 16 +}; diff --git a/packages/string-store/src/lib/types/Int2.ts b/packages/string-store/src/lib/types/Int2.ts new file mode 100644 index 0000000000..c2f84fb1f1 --- /dev/null +++ b/packages/string-store/src/lib/types/Int2.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Int2Type: IType = { + serialize(buffer, value) { + buffer.writeInt2(value); + }, + deserialize(buffer, pointer) { + return buffer.readInt2(pointer); + }, + BIT_SIZE: 2 +}; diff --git a/packages/string-store/src/lib/types/Int32.ts b/packages/string-store/src/lib/types/Int32.ts new file mode 100644 index 0000000000..a60eefc32e --- /dev/null +++ b/packages/string-store/src/lib/types/Int32.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Int32Type: IType = { + serialize(buffer, value) { + buffer.writeInt32(value); + }, + deserialize(buffer, pointer) { + return buffer.readInt32(pointer); + }, + BIT_SIZE: 32 +}; diff --git a/packages/string-store/src/lib/types/Int4.ts b/packages/string-store/src/lib/types/Int4.ts new file mode 100644 index 0000000000..9e10942793 --- /dev/null +++ b/packages/string-store/src/lib/types/Int4.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Int4Type: IType = { + serialize(buffer, value) { + buffer.writeInt4(value); + }, + deserialize(buffer, pointer) { + return buffer.readInt4(pointer); + }, + BIT_SIZE: 4 +}; diff --git a/packages/string-store/src/lib/types/Int64.ts b/packages/string-store/src/lib/types/Int64.ts new file mode 100644 index 0000000000..15b7e13a6a --- /dev/null +++ b/packages/string-store/src/lib/types/Int64.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Int64Type: IType = { + serialize(buffer, value) { + buffer.writeInt64(value); + }, + deserialize(buffer, pointer) { + return buffer.readInt64(pointer); + }, + BIT_SIZE: 64 +}; diff --git a/packages/string-store/src/lib/types/Int8.ts b/packages/string-store/src/lib/types/Int8.ts new file mode 100644 index 0000000000..dec9acb93d --- /dev/null +++ b/packages/string-store/src/lib/types/Int8.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const Int8Type: IType = { + serialize(buffer, value) { + buffer.writeInt8(value); + }, + deserialize(buffer, pointer) { + return buffer.readInt8(pointer); + }, + BIT_SIZE: 8 +}; diff --git a/packages/string-store/src/lib/types/Snowflake.ts b/packages/string-store/src/lib/types/Snowflake.ts new file mode 100644 index 0000000000..258858ab09 --- /dev/null +++ b/packages/string-store/src/lib/types/Snowflake.ts @@ -0,0 +1,11 @@ +import type { IType } from './base/IType'; + +export const SnowflakeType = { + serialize(buffer, value: bigint | string) { + buffer.writeBigInt64(BigInt(value)); + }, + deserialize(buffer, offset) { + return buffer.readBigInt64(offset); + }, + BIT_SIZE: 64 +} as const satisfies IType; diff --git a/packages/string-store/src/lib/types/String.ts b/packages/string-store/src/lib/types/String.ts new file mode 100644 index 0000000000..e36c62c3c1 --- /dev/null +++ b/packages/string-store/src/lib/types/String.ts @@ -0,0 +1,22 @@ +import type { IType } from './base/IType'; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +export const StringType: IType = { + serialize(buffer, value) { + const encoded = encoder.encode(value); + buffer.writeInt16(encoded.length); + for (const byte of encoded) { + buffer.writeInt8(byte); + } + }, + deserialize(buffer, pointer) { + const length = buffer.readInt16(pointer); + const bytes = new Uint8Array(length); + for (let i = 0; i < length; i++) { + bytes[i] = buffer.readInt8(pointer); + } + return decoder.decode(bytes); + }, + BIT_SIZE: null +}; diff --git a/packages/string-store/src/lib/types/base/IType.ts b/packages/string-store/src/lib/types/base/IType.ts new file mode 100644 index 0000000000..0b5a6b10af --- /dev/null +++ b/packages/string-store/src/lib/types/base/IType.ts @@ -0,0 +1,25 @@ +import type { Pointer } from '../../shared/Pointer'; +import type { UnalignedUint16Array } from '../../UnalignedUint16Array'; + +export interface IType { + /** + * Serialize a value to a buffer. + * + * @param buffer The buffer to write to + * @param value The value to write + */ + serialize(buffer: UnalignedUint16Array, value: Readonly): void; + + /** + * Deserialize a value from a buffer. + * + * @param buffer The buffer to read from + * @param pointer The pointer indicating the current position in the buffer + */ + deserialize(buffer: UnalignedUint16Array, pointer: Pointer): ValueType; + + /** + * The size of the value in bits, or `null` if the size is variable. + */ + readonly BIT_SIZE: BitSize; +} diff --git a/packages/string-store/src/tsconfig.json b/packages/string-store/src/tsconfig.json new file mode 100644 index 0000000000..ad55754826 --- /dev/null +++ b/packages/string-store/src/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "../dist", + "incremental": false + }, + "include": ["."] +} diff --git a/packages/string-store/tests/lib/Schema.test.ts b/packages/string-store/tests/lib/Schema.test.ts new file mode 100644 index 0000000000..d6301c0bbe --- /dev/null +++ b/packages/string-store/tests/lib/Schema.test.ts @@ -0,0 +1,310 @@ +import { + BigInt32Type, + BigInt64Type, + BitType, + BooleanType, + Float32Type, + Float64Type, + Int16Type, + Int2Type, + Int32Type, + Int4Type, + Int64Type, + Int8Type, + Pointer, + Schema, + SnowflakeType, + StringType, + UnalignedUint16Array, + type IType +} from '../../src'; + +describe('Schema', () => { + test('GIVEN a new instance THEN it has the correct properties', () => { + const schema = new Schema(1); + expectTypeOf>(schema); + expect<1>(schema.id).toBe(1); + expect(schema.bitSize).toBe(0); + + type Key = string; + type Value = never; + type Entry = readonly [Key, Value]; + + expect([...schema.keys()]).toEqual([]); + expect([...schema.values()]).toEqual([]); + expect([...schema.entries()]).toEqual([]); + expect([...schema]).toEqual([]); + }); + + describe('types', () => { + test('GIVEN a schema with a boolean property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof BooleanType; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).boolean('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(1); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([BooleanType]); + expect([...schema.entries()]).toEqual([['a', BooleanType]]); + }); + + test('GIVEN a schema with a bit property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof BitType; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).bit('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(1); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([BitType]); + expect([...schema.entries()]).toEqual([['a', BitType]]); + }); + + test('GIVEN a schema with an int2 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Int2Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).int2('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(2); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Int2Type]); + expect([...schema.entries()]).toEqual([['a', Int2Type]]); + }); + + test('GIVEN a schema with an int4 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Int4Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).int4('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(4); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Int4Type]); + expect([...schema.entries()]).toEqual([['a', Int4Type]]); + }); + + test('GIVEN a schema with an int8 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Int8Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).int8('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(8); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Int8Type]); + expect([...schema.entries()]).toEqual([['a', Int8Type]]); + }); + + test('GIVEN a schema with an int16 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Int16Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).int16('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(16); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Int16Type]); + expect([...schema.entries()]).toEqual([['a', Int16Type]]); + }); + + test('GIVEN a schema with an int32 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Int32Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).int32('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(32); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Int32Type]); + expect([...schema.entries()]).toEqual([['a', Int32Type]]); + }); + + test('GIVEN a schema with an int64 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Int64Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).int64('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(64); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Int64Type]); + expect([...schema.entries()]).toEqual([['a', Int64Type]]); + }); + + test('GIVEN a schema with a bigInt32 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof BigInt32Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).bigInt32('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(32); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([BigInt32Type]); + expect([...schema.entries()]).toEqual([['a', BigInt32Type]]); + }); + + test('GIVEN a schema with a bigInt64 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof BigInt64Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).bigInt64('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(64); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([BigInt64Type]); + expect([...schema.entries()]).toEqual([['a', BigInt64Type]]); + }); + + test('GIVEN a schema with a float32 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Float32Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).float32('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(32); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Float32Type]); + expect([...schema.entries()]).toEqual([['a', Float32Type]]); + }); + + test('GIVEN a schema with a float64 property THEN it has the correct properties and types', () => { + type Key = 'a'; + type Value = typeof Float64Type; + type Entry = readonly [Key, Value]; + + const schema = new Schema(1).float64('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(64); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([Float64Type]); + expect([...schema.entries()]).toEqual([['a', Float64Type]]); + }); + + test('GIVEN a schema with a string property THEN it has the correct properties and types', () => { + const schema = new Schema(1).string('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBeNull(); + + type Key = 'a'; + type Value = typeof StringType; + type Entry = readonly [Key, Value]; + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([StringType]); + expect([...schema.entries()]).toEqual([['a', StringType]]); + }); + + test('GIVEN a schema with a snowflake property THEN it has the correct properties and types', () => { + const schema = new Schema(1).snowflake('a'); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(64); + + type Key = 'a'; + type Value = typeof SnowflakeType; + type Entry = readonly [Key, Value]; + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([SnowflakeType]); + expect([...schema.entries()]).toEqual([['a', SnowflakeType]]); + }); + + test('GIVEN a schema with an array THEN it has the correct properties and types', () => { + const schema = new Schema(1).array('a', BitType); + expectTypeOf>(schema); + expect(schema.bitSize).toBeNull(); + + type Key = 'a'; + type Value = IType; + type Entry = readonly [Key, Value]; + + const value = schema.get('a'); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([value]); + expect([...schema.entries()]).toEqual([['a', value]]); + }); + + test('GIVEN a schema with a fixed-length array THEN it has the correct properties and types', () => { + const schema = new Schema(1).fixedLengthArray('a', BitType, 2); + expectTypeOf>(schema); + expect(schema.bitSize).toBe(2); + + type Key = 'a'; + type Value = IType; + type Entry = readonly [Key, Value]; + + const value = schema.get('a'); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([value]); + expect([...schema.entries()]).toEqual([['a', value]]); + }); + + test('GIVEN a schema with a fixed-length array of an element with no length THEN it has the correct properties and types', () => { + const schema = new Schema(1).fixedLengthArray('a', StringType, 2); + expectTypeOf>(schema); + expect(schema.bitSize).toBeNull(); + + type Key = 'a'; + type Value = IType; + type Entry = readonly [Key, Value]; + + const value = schema.get('a'); + + expect([...schema.keys()]).toEqual(['a']); + expect([...schema.values()]).toEqual([value]); + expect([...schema.entries()]).toEqual([['a', value]]); + }); + }); + + describe('serialization', () => { + test('GIVEN a schema with a boolean property THEN it serializes correctly', () => { + const buffer = new UnalignedUint16Array(2); + const schema = new Schema(4).boolean('a'); + + schema.serialize(buffer, { a: true }); + expect(buffer.toArray()).toEqual(new Uint16Array([4, 1])); + + const value = schema.deserialize(buffer, 16); + expect<{ a: boolean }>(value).toEqual({ a: true }); + }); + }); + + describe('exceptions', () => { + test('GIVEN a schema with a duplicate property THEN it throws', () => { + const schema = new Schema(1).boolean('a'); + expect(() => schema.boolean('a')).toThrowError('Schema with id 1 already has a property with name "a"'); + }); + + test('GIVEN a schema with a non-existent property name THEN it throws', () => { + const schema = new Schema(1).boolean('a'); + // @ts-expect-error Testing invalid input + expect(() => schema.get('b')).toThrowError('Schema with id 1 does not have a property with name "b"'); + }); + }); +}); diff --git a/packages/string-store/tests/lib/SchemaStore.test.ts b/packages/string-store/tests/lib/SchemaStore.test.ts new file mode 100644 index 0000000000..15fc9f6771 --- /dev/null +++ b/packages/string-store/tests/lib/SchemaStore.test.ts @@ -0,0 +1,58 @@ +import { Float64Type, Schema, SchemaStore } from '../../src'; + +describe('SchemaStore', () => { + test('GIVEN an empty SchemaStore THEN it should be empty', () => { + const store = new SchemaStore(); + expectTypeOf>(store); + expect(store.defaultMaximumArrayLength).toBe(100); + + type Key = string; + type Value = never; + type Entry = readonly [Key, Value]; + + expect([...store.keys()]).toEqual([]); + expect([...store.values()]).toEqual([]); + expect([...store.entries()]).toEqual([]); + expect([...store]).toEqual([]); + }); + + test('GIVEN a SchemaStore with a Schema THEN it has the correct properties and types', () => { + const schema = new Schema(2).float64('height'); + const store = new SchemaStore().add(schema); + expectTypeOf }>>(store); + + expect(store.get(2)).toBe(schema); + }); + + describe('serialization', () => { + test('GIVEN a schema and a value THEN it serializes and deserializes the buffer correctly', () => { + const store = new SchemaStore(10).add(new Schema(2).string('name').float64('height')); + + const buffer = store.serialize(2, { name: 'Mario', height: 1.8 }); + const deserialized = store.deserialize(buffer); + expect<{ id: 2; data: { height: number } }>(deserialized).toEqual({ id: 2, data: { name: 'Mario', height: 1.8 } }); + }); + + test('GIVEN a schema and a value THEN it serializes and deserializes the binary string correctly', () => { + const store = new SchemaStore(10).add(new Schema(2).string('name').float64('height')); + + const buffer = store.serialize(2, { name: 'Mario', height: 1.8 }); + const deserialized = store.deserialize(buffer.toString()); + expect<{ id: 2; data: { height: number } }>(deserialized).toEqual({ id: 2, data: { name: 'Mario', height: 1.8 } }); + }); + }); + + describe('exceptions', () => { + test('GIVEN a schema ID that already exists THEN it throws', () => { + const schema = new Schema(2).float64('height'); + const store = new SchemaStore().add(schema); + expect(() => store.add(schema)).toThrowError('Schema with id 2 already exists'); + }); + + test('GIVEN a schema ID that does not exist THEN it throws', () => { + const store = new SchemaStore(); + // @ts-expect-error Testing invalid input + expect(() => store.get(2)).toThrowError('Schema with id 2 does not exist'); + }); + }); +}); diff --git a/packages/string-store/tests/lib/UnalignedUint16Array.test.ts b/packages/string-store/tests/lib/UnalignedUint16Array.test.ts new file mode 100644 index 0000000000..001a44a6ed --- /dev/null +++ b/packages/string-store/tests/lib/UnalignedUint16Array.test.ts @@ -0,0 +1,150 @@ +import { UnalignedUint16Array } from '../../src'; + +describe('UnalignedUint16Array', () => { + test('GIVEN a new instance THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + expect(buffer.maxLength).toBe(10); + expect(buffer.maxBitLength).toBe(160); + expect(buffer.length).toBe(0); + expect(buffer.bitLength).toBe(0); + expect(buffer.toArray()).toEqual(new Uint16Array(0)); + expect(buffer.toString()).toBe(''); + }); + + test('GIVEN an instance with a bit written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeBit(1); + expect(buffer.length).toBe(1); + expect(buffer.bitLength).toBe(1); + expect(buffer.toArray()).toEqual(new Uint16Array([1])); + expect(buffer.toString()).toBe('\u{1}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readBit(0)).toBe(1); + }); + + test('GIVEN an instance with a 2-bit integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeInt2(3); + expect(buffer.length).toBe(1); + expect(buffer.bitLength).toBe(2); + expect(buffer.toArray()).toEqual(new Uint16Array([3])); + expect(buffer.toString()).toBe('\u{3}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readInt2(0)).toBe(3); + }); + + test('GIVEN an instance with a 4-bit integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeInt4(5); + expect(buffer.length).toBe(1); + expect(buffer.bitLength).toBe(4); + expect(buffer.toArray()).toEqual(new Uint16Array([5])); + expect(buffer.toString()).toBe('\u{5}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readInt4(0)).toBe(5); + }); + + test('GIVEN an instance with an 8-bit integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeInt8(255); + expect(buffer.length).toBe(1); + expect(buffer.bitLength).toBe(8); + expect(buffer.toArray()).toEqual(new Uint16Array([255])); + expect(buffer.toString()).toBe('\u{ff}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readInt8(0)).toBe(255); + }); + + test('GIVEN an instance with a 16-bit integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeInt16(65535); + expect(buffer.length).toBe(1); + expect(buffer.bitLength).toBe(16); + expect(buffer.toArray()).toEqual(new Uint16Array([65535])); + expect(buffer.toString()).toBe('\u{ffff}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readInt16(0)).toBe(65535); + }); + + test('GIVEN an instance with a 32-bit integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeInt32(4294967295); + expect(buffer.length).toBe(2); + expect(buffer.bitLength).toBe(32); + expect(buffer.toArray()).toEqual(new Uint16Array([65535, 65535])); + expect(buffer.toString()).toBe('\u{ffff}\u{ffff}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readInt32(0)).toBe(4294967295); + }); + + test('GIVEN an instance with a 64-bit integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeInt64(3058204829589); + expect(buffer.length).toBe(4); + expect(buffer.bitLength).toBe(64); + expect(buffer.toArray()).toEqual(new Uint16Array([26517, 2870, 712, 0])); + expect(buffer.toString()).toBe('\u{6795}\u{0b36}\u{02c8}\0'); + + // Verify that the data can be deserialized correctly + expect(buffer.readInt64(0)).toBe(3058204829589); + }); + + test('GIVEN an instance with a 32-bit big integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeBigInt32(4294967295n); + expect(buffer.length).toBe(2); + expect(buffer.bitLength).toBe(32); + expect(buffer.toArray()).toEqual(new Uint16Array([65535, 65535])); + expect(buffer.toString()).toBe('\u{ffff}\u{ffff}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readBigInt32(0)).toBe(4294967295n); + }); + + test('GIVEN an instance with a 64-bit big integer written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeBigInt64(18446744073709551615n); + expect(buffer.length).toBe(4); + expect(buffer.bitLength).toBe(64); + expect(buffer.toArray()).toEqual(new Uint16Array([65535, 65535, 65535, 65535])); + expect(buffer.toString()).toBe('\u{ffff}\u{ffff}\u{ffff}\u{ffff}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readBigInt64(0)).toBe(18446744073709551615n); + }); + + test('GIVEN an instance with a 32-bit float written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeFloat32(0.5); + expect(buffer.length).toBe(2); + expect(buffer.bitLength).toBe(32); + expect(buffer.toArray()).toEqual(new Uint16Array([0, 16128])); + expect(buffer.toString()).toBe('\0\u{3f00}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readFloat32(0)).toBe(0.5); + }); + + test('GIVEN an instance with a 64-bit float written THEN it has the correct properties', () => { + const buffer = new UnalignedUint16Array(10); + buffer.writeFloat64(0.5); + expect(buffer.length).toBe(4); + expect(buffer.bitLength).toBe(64); + expect(buffer.toArray()).toEqual(new Uint16Array([0, 0, 0, 16352])); + expect(buffer.toString()).toBe('\0\0\0\u{3fe0}'); + + // Verify that the data can be deserialized correctly + expect(buffer.readFloat64(0)).toBe(0.5); + }); + + test('GIVEN an instance with insufficient space THEN it throws', () => { + const buffer = new UnalignedUint16Array(1); + expect(() => buffer.writeInt32(1)).toThrowError('The buffer is full'); + }); +}); diff --git a/packages/string-store/tests/lib/shared/Pointer.test.ts b/packages/string-store/tests/lib/shared/Pointer.test.ts new file mode 100644 index 0000000000..2a5b5ba17f --- /dev/null +++ b/packages/string-store/tests/lib/shared/Pointer.test.ts @@ -0,0 +1,32 @@ +import { Pointer } from '../../../src'; + +describe('Pointer', () => { + test('GIVEN a pointer THEN it has the correct initial properties', () => { + const pointer = new Pointer(); + expect(pointer.value).toBe(0); + }); + + test('GIVEN a pointer with an added value THEN returns the correct values', () => { + const pointer = new Pointer(); + pointer.add(5); + expect(pointer.value).toBe(5); + }); + + test.each(['foo', () => {}, -1, 2147483648])('GIVEN a pointer with an invalid length value THEN throws', (value) => { + const pointer = new Pointer(); + // @ts-expect-error Testing invalid input + expect(() => pointer.add(value)).toThrowError('The pointer value cannot be an invalid length value'); + }); + + describe('from', () => { + test('GIVEN a pointer with a value THEN it returns a new instance with the same value', () => { + const pointer = Pointer.from(5); + expect(pointer.value).toBe(5); + }); + + test.each(['foo', () => {}, -1, 2147483648, 5.5])('GIVEN an invalid length value THEN throws', (value) => { + // @ts-expect-error Testing invalid input + expect(() => Pointer.from(value)).toThrowError('The pointer value cannot be an invalid length value'); + }); + }); +}); diff --git a/packages/string-store/tests/lib/shared/common.test.ts b/packages/string-store/tests/lib/shared/common.test.ts new file mode 100644 index 0000000000..ce0ce42e27 --- /dev/null +++ b/packages/string-store/tests/lib/shared/common.test.ts @@ -0,0 +1,38 @@ +import { isArrayLike, isValidLength } from '../../../src/lib/shared/_common'; + +describe('isArrayLike', () => { + test.each([undefined, null, 42, 100n, () => {}, Symbol('foo'), 'bar'])('GIVEN a non-object or null value THEN it returns false', (value) => { + expect(isArrayLike(value)).toBe(false); + }); + + test('GIVEN an array THEN it returns true', () => { + expect(isArrayLike([])).toBe(true); + }); + + test.each([{}, { length: '42' }])('GIVEN an object without a numeric length property THEN it returns false', (value) => { + expect(isArrayLike(value)).toBe(false); + }); + + test('GIVEN an object with a numeric length property and a length of 0 THEN it returns true', () => { + expect(isArrayLike({ length: 0 })).toBe(true); + }); + + test('GIVEN an object with a non-zero length and no last property THEN it returns false', () => { + expect(isArrayLike({ length: 1 })).toBe(false); + }); + + test('GIVEN an object with a non-zero length and has the last property THEN it returns true', () => { + expect(isArrayLike({ length: 1, 0: 'foo' })).toBe(true); + }); +}); + +describe('isValidLength', () => { + test.each([undefined, null, 4.2, 100n, () => {}, Symbol('foo'), 'bar'])('GIVEN a non-safe integer THEN it returns false', (value) => { + // @ts-expect-error Testing invalid input + expect(isValidLength(value)).toBe(false); + }); + + test.each([-1, 2147483648, Number.MAX_SAFE_INTEGER, Infinity, -Infinity, NaN])('GIVEN an out-of-range length THEN it returns false', (value) => { + expect(isValidLength(value)).toBe(false); + }); +}); diff --git a/packages/string-store/tests/lib/types.test.ts b/packages/string-store/tests/lib/types.test.ts new file mode 100644 index 0000000000..6bc9bc00a7 --- /dev/null +++ b/packages/string-store/tests/lib/types.test.ts @@ -0,0 +1,312 @@ +import { + ArrayType, + BigInt32Type, + BigInt64Type, + BitType, + BooleanType, + FixedLengthArrayType, + Float32Type, + Float64Type, + Int16Type, + Int2Type, + Int32Type, + Int4Type, + Int64Type, + Int8Type, + Pointer, + SnowflakeType, + StringType, + UnalignedUint16Array +} from '../../src'; + +describe('types', () => { + describe('Boolean', () => { + const type = BooleanType; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(1); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, true); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(true); + }); + }); + + describe('Bit', () => { + const type = BitType; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(1); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 1); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(1); + }); + }); + + describe('Int2', () => { + const type = Int2Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(2); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 3); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(3); + }); + }); + + describe('Int4', () => { + const type = Int4Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(4); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 5); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(5); + }); + }); + + describe('Int8', () => { + const type = Int8Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(8); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 255); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(255); + }); + }); + + describe('Int16', () => { + const type = Int16Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(16); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 65535); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(65535); + }); + }); + + describe('Int32', () => { + const type = Int32Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(32); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 4294967295); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(4294967295); + }); + }); + + describe('Int64', () => { + const type = Int64Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(64); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 3058204829589); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(3058204829589); + }); + }); + + describe('BigInt32', () => { + const type = BigInt32Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(32); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 4294967295n); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(4294967295n); + }); + }); + + describe('BigInt64', () => { + const type = BigInt64Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(64); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 18446744073709551615n); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(18446744073709551615n); + }); + }); + + describe('Float32', () => { + const type = Float32Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(32); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 1.1); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toBeCloseTo(1.1); + }); + }); + + describe('Float64', () => { + const type = Float64Type; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(64); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 1.1); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toBeCloseTo(1.1); + }); + }); + + describe('Array(Bit)', () => { + const type = ArrayType(BitType); + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBeNull(); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, [1, 0, 1, 0, 1]); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual([1, 0, 1, 0, 1]); + }); + + test.each(['Foo', () => {}, true, 42, 100n, Symbol('foo'), {}, { length: '0' }, { length: 1 }])( + 'GIVEN an invalid value THEN it throws an error', + (value) => { + const buffer = new UnalignedUint16Array(10); + // @ts-expect-error Testing invalid input + expect(() => type.serialize(buffer, value)).toThrowError(); + } + ); + }); + + describe('FixedLengthArray(Bit)', () => { + const type = FixedLengthArrayType(BitType, 2); + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(2); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, [1, 0]); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual([1, 0]); + }); + + test.each(['Foo', () => {}, true, 42, 100n, Symbol('foo'), {}, { length: '0' }, { length: 1 }, { length: 1.5 }, [], [1, 2, 3]])( + 'GIVEN an invalid value THEN it throws an error', + (value) => { + const buffer = new UnalignedUint16Array(10); + // @ts-expect-error Testing invalid input + expect(() => type.serialize(buffer, value)).toThrowError(); + } + ); + }); + + describe('String', () => { + const type = StringType; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBeNull(); + }); + + test('GIVEN a buffer THEN it serializes and deserializes correctly', () => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, 'Hello, World!'); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual('Hello, World!'); + }); + }); + + describe('Snowflake', () => { + const type = SnowflakeType; + + test('GIVEN type THEN it has the correct properties', () => { + expect(type.BIT_SIZE).toBe(64); + }); + + test.each([737141877803057244n, '737141877803057244'])('GIVEN a buffer THEN it serializes and deserializes correctly', (value) => { + const buffer = new UnalignedUint16Array(10); + + type.serialize(buffer, value); + + const deserialized = type.deserialize(buffer, new Pointer()); + expect(deserialized).toEqual(737141877803057244n); + }); + }); +}); diff --git a/packages/string-store/tests/tsconfig.json b/packages/string-store/tests/tsconfig.json new file mode 100644 index 0000000000..8312de11d9 --- /dev/null +++ b/packages/string-store/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2021", + "noEmit": true, + "incremental": false, + "types": ["vitest/globals"] + }, + "include": ["./"] +} diff --git a/packages/string-store/tsconfig.eslint.json b/packages/string-store/tsconfig.eslint.json new file mode 100644 index 0000000000..ff36d901a1 --- /dev/null +++ b/packages/string-store/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true + }, + "include": ["src", "tests"] +} diff --git a/packages/string-store/tsup.config.ts b/packages/string-store/tsup.config.ts new file mode 100644 index 0000000000..b7010dd0e0 --- /dev/null +++ b/packages/string-store/tsup.config.ts @@ -0,0 +1,5 @@ +import { createTsupConfig } from '../../scripts/tsup.config'; + +export default createTsupConfig({ + iifeOptions: { globalName: 'SapphireStringStore' } +}); diff --git a/packages/string-store/typedoc.json b/packages/string-store/typedoc.json new file mode 100644 index 0000000000..8c791b5134 --- /dev/null +++ b/packages/string-store/typedoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"], + "json": "docs/api.json", + "tsconfig": "src/tsconfig.json" +} diff --git a/packages/string-store/vitest.config.ts b/packages/string-store/vitest.config.ts new file mode 100644 index 0000000000..5223a8a212 --- /dev/null +++ b/packages/string-store/vitest.config.ts @@ -0,0 +1,3 @@ +import { createVitestConfig } from '../../scripts/vitest.config'; + +export default createVitestConfig(); diff --git a/scripts/clean-full.mjs b/scripts/clean-full.mjs index 84a249736b..f0e08edc04 100644 --- a/scripts/clean-full.mjs +++ b/scripts/clean-full.mjs @@ -28,6 +28,7 @@ const paths = [ new URL('result/node_modules/', packagesDir), new URL('snowflake/node_modules/', packagesDir), new URL('stopwatch/node_modules/', packagesDir), + new URL('string-store/node_modules/', packagesDir), new URL('time-utilities/node_modules/', packagesDir), new URL('timer-manager/node_modules/', packagesDir), new URL('timestamp/node_modules/', packagesDir), @@ -54,6 +55,7 @@ const paths = [ new URL('result/dist/', packagesDir), new URL('snowflake/dist/', packagesDir), new URL('stopwatch/dist/', packagesDir), + new URL('string-store/dist/', packagesDir), new URL('time-utilities/dist/', packagesDir), new URL('timer-manager/dist/', packagesDir), new URL('timestamp/dist/', packagesDir), diff --git a/scripts/clean.mjs b/scripts/clean.mjs index 68fdad8a8f..3da342ed07 100644 --- a/scripts/clean.mjs +++ b/scripts/clean.mjs @@ -25,6 +25,7 @@ const paths = [ new URL('result/dist/', packagesDir), new URL('snowflake/dist/', packagesDir), new URL('stopwatch/dist/', packagesDir), + new URL('string-store/dist/', packagesDir), new URL('time-utilities/dist/', packagesDir), new URL('timer-manager/dist/', packagesDir), new URL('timestamp/dist/', packagesDir), @@ -50,6 +51,7 @@ const paths = [ new URL('result/.turbo/', packagesDir), new URL('snowflake/.turbo/', packagesDir), new URL('stopwatch/.turbo/', packagesDir), + new URL('string-store/.turbo/', packagesDir), new URL('time-utilities/.turbo/', packagesDir), new URL('timer-manager/.turbo/', packagesDir), new URL('timestamp/.turbo/', packagesDir), diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 35207d0667..42c561a62b 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,26 +1,27 @@ import { defineWorkspace } from 'vitest/config'; export default defineWorkspace([ - './packages/iterator-utilities/vitest.config.ts', + './packages/async-queue/vitest.config.ts', + './packages/bitfield/vitest.config.ts', + './packages/cron/vitest.config.ts', + './packages/decorators/vitest.config.ts', './packages/discord-utilities/vitest.config.ts', - './packages/lexure/vitest.config.ts', + './packages/duration/vitest.config.ts', + './packages/eslint-config/vitest.config.ts', './packages/eslint-plugin-result/vitest.config.ts', - './packages/result/vitest.config.ts', - './packages/async-queue/vitest.config.ts', - './packages/ts-config/vitest.config.ts', - './packages/utilities/vitest.config.ts', - './packages/timestamp/vitest.config.ts', - './packages/timer-manager/vitest.config.ts', - './packages/stopwatch/vitest.config.ts', - './packages/snowflake/vitest.config.ts', - './packages/ratelimits/vitest.config.ts', - './packages/fetch/vitest.config.ts', './packages/event-iterator/vitest.config.ts', + './packages/fetch/vitest.config.ts', + './packages/iterator-utilities/vitest.config.ts', + './packages/lexure/vitest.config.ts', './packages/node-utilities/vitest.config.ts', './packages/prettier-config/vitest.config.ts', - './packages/decorators/vitest.config.ts', - './packages/duration/vitest.config.ts', - './packages/bitfield/vitest.config.ts', - './packages/eslint-config/vitest.config.ts', - './packages/cron/vitest.config.ts' + './packages/ratelimits/vitest.config.ts', + './packages/result/vitest.config.ts', + './packages/snowflake/vitest.config.ts', + './packages/stopwatch/vitest.config.ts', + './packages/string-store/vitest.config.ts', + './packages/timer-manager/vitest.config.ts', + './packages/timestamp/vitest.config.ts', + './packages/ts-config/vitest.config.ts', + './packages/utilities/vitest.config.ts' ]); diff --git a/yarn.lock b/yarn.lock index 67ce20060a..117eb814d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1681,6 +1681,21 @@ __metadata: languageName: unknown linkType: soft +"@sapphire/string-store@workspace:packages/string-store": + version: 0.0.0-use.local + resolution: "@sapphire/string-store@workspace:packages/string-store" + dependencies: + "@favware/cliff-jumper": "npm:^4.0.2" + "@vitest/coverage-v8": "npm:^2.0.2" + tsup: "npm:^8.1.0" + tsx: "npm:^4.16.2" + typedoc: "npm:^0.25.13" + typedoc-json-parser: "npm:^10.0.0" + typescript: "npm:~5.4.5" + vitest: "npm:^2.0.2" + languageName: unknown + linkType: soft + "@sapphire/time-utilities@workspace:packages/time-utilities": version: 0.0.0-use.local resolution: "@sapphire/time-utilities@workspace:packages/time-utilities" @@ -2028,9 +2043,9 @@ __metadata: languageName: node linkType: hard -"@vitest/coverage-v8@npm:^2.0.3": - version: 2.0.3 - resolution: "@vitest/coverage-v8@npm:2.0.3" +"@vitest/coverage-v8@npm:^2.0.2, @vitest/coverage-v8@npm:^2.0.3": + version: 2.0.4 + resolution: "@vitest/coverage-v8@npm:2.0.4" dependencies: "@ampproject/remapping": "npm:^2.3.0" "@bcoe/v8-coverage": "npm:^0.2.3" @@ -2042,75 +2057,74 @@ __metadata: magic-string: "npm:^0.30.10" magicast: "npm:^0.3.4" std-env: "npm:^3.7.0" - strip-literal: "npm:^2.1.0" test-exclude: "npm:^7.0.1" tinyrainbow: "npm:^1.2.0" peerDependencies: - vitest: 2.0.3 - checksum: 10/776b10e2ed5e9e3cb1a74676ddc31033164a3d465b84dec95f817c67b69efc1e6297c64d1d29412b3966557ab5c32aa4bc04ba8506ca13369dd31edacb11107e + vitest: 2.0.4 + checksum: 10/de23ca9c8e7cd704d889475af9a8282a1d29e4ca05909edc28df3f55b65a10cba344ab350ab255d0ad1b8a3dc6d98ebb12d5c2614bc8d92b03b645f15912ae61 languageName: node linkType: hard -"@vitest/expect@npm:2.0.3": - version: 2.0.3 - resolution: "@vitest/expect@npm:2.0.3" +"@vitest/expect@npm:2.0.4": + version: 2.0.4 + resolution: "@vitest/expect@npm:2.0.4" dependencies: - "@vitest/spy": "npm:2.0.3" - "@vitest/utils": "npm:2.0.3" + "@vitest/spy": "npm:2.0.4" + "@vitest/utils": "npm:2.0.4" chai: "npm:^5.1.1" tinyrainbow: "npm:^1.2.0" - checksum: 10/f47d126c4c2f685c7525cf826f21b826fc9fa8c9a9fc9b42ff4d6c71ac843094b9ae033b29dd88b2f5558815e040451f1e037ce61e8de907db13b1a02f51bc8a + checksum: 10/9e77266306a9ee6c982956e79e5086edeaec9f387fb9f8840d749ba9e026b27c01f68987a732b53746cd7fb0fce4a2620dbd0359ca3efe891a8ba89300568111 languageName: node linkType: hard -"@vitest/pretty-format@npm:2.0.3, @vitest/pretty-format@npm:^2.0.3": - version: 2.0.3 - resolution: "@vitest/pretty-format@npm:2.0.3" +"@vitest/pretty-format@npm:2.0.4, @vitest/pretty-format@npm:^2.0.4": + version: 2.0.4 + resolution: "@vitest/pretty-format@npm:2.0.4" dependencies: tinyrainbow: "npm:^1.2.0" - checksum: 10/5a4289ecaacec356212afd44c1fa25accf2fbda4640021b304154671c675dc58d203c9422eea9e08eac2426755b33e02597f56fdb515320566672d5f241e1266 + checksum: 10/16223d1c9f8c86cea7a064cf625380e90b20a5c2f95fda6ab3643c16cce1925afa337109ee12dcbf54834a161fd2b68be16179da9fd9fb948de986c33942203b languageName: node linkType: hard -"@vitest/runner@npm:2.0.3": - version: 2.0.3 - resolution: "@vitest/runner@npm:2.0.3" +"@vitest/runner@npm:2.0.4": + version: 2.0.4 + resolution: "@vitest/runner@npm:2.0.4" dependencies: - "@vitest/utils": "npm:2.0.3" + "@vitest/utils": "npm:2.0.4" pathe: "npm:^1.1.2" - checksum: 10/a1f41f0d1edbfd9dbff2616d0eb40964eb1764ef61fdc0013e014aa3343968645d9c83e091d69192450887914c90f9a8dc9db52da3b56ea6bf1ce4b240b3cbd3 + checksum: 10/a94872a08296b72316d1259fa8f12e314a47614b614cba03f1d0ba7f00e82f5d724b740ab17b8f6ddbe281acea278dec212f5050ac557b108df8f50b7aab6cbd languageName: node linkType: hard -"@vitest/snapshot@npm:2.0.3": - version: 2.0.3 - resolution: "@vitest/snapshot@npm:2.0.3" +"@vitest/snapshot@npm:2.0.4": + version: 2.0.4 + resolution: "@vitest/snapshot@npm:2.0.4" dependencies: - "@vitest/pretty-format": "npm:2.0.3" + "@vitest/pretty-format": "npm:2.0.4" magic-string: "npm:^0.30.10" pathe: "npm:^1.1.2" - checksum: 10/d1529b93f853731bb352feb6b00921711407f0fbf829b0b359027f779f5f23c36abba4a7187c41a13376f45b769fe581cb6f60c22b2bdee6ce92a80c0fa49564 + checksum: 10/bbdc491d42a95945589a7006ef40beb199332b28b5832f111bd25e26b24bd78134efdb05b670e65dc82f83c654e1aedc445c26be20bdaa758a6c3cf844bd05b5 languageName: node linkType: hard -"@vitest/spy@npm:2.0.3": - version: 2.0.3 - resolution: "@vitest/spy@npm:2.0.3" +"@vitest/spy@npm:2.0.4": + version: 2.0.4 + resolution: "@vitest/spy@npm:2.0.4" dependencies: tinyspy: "npm:^3.0.0" - checksum: 10/cab153e2e071528c5652057a48fd4c5c72af6bcea460b8e036bd83ec734e85d190b8cbac75faafacf0c76c500641cf03c22ea806794951d3b291c5113cf82a28 + checksum: 10/c18d0fc28e40a40f701a116a117d98916ec90f18e1643a37379b18f5fbee841e7c35fcb65202503506b471df761e0907053912d475e159399b887c1be6f91ef1 languageName: node linkType: hard -"@vitest/utils@npm:2.0.3": - version: 2.0.3 - resolution: "@vitest/utils@npm:2.0.3" +"@vitest/utils@npm:2.0.4": + version: 2.0.4 + resolution: "@vitest/utils@npm:2.0.4" dependencies: - "@vitest/pretty-format": "npm:2.0.3" + "@vitest/pretty-format": "npm:2.0.4" estree-walker: "npm:^3.0.3" loupe: "npm:^3.1.1" tinyrainbow: "npm:^1.2.0" - checksum: 10/e7785b4fd0ff51dff7e7a918b9653f1c2d36dda0d01e0c3222bc1ee7b519034fcd7485b33e52c566b124def2afb449db6a8e53416a615a233058bfa95a1def52 + checksum: 10/a17497cd3c12b72b315bda6a6a4addcbc206367f6bcdedb83d5d708ac40cf52fcc48403539d10528e1893348b2f107416e9065b6b5c39329f2512eea8f104578 languageName: node linkType: hard @@ -3672,18 +3686,6 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.1.1": - version: 6.1.1 - resolution: "fdir@npm:6.1.1" - peerDependencies: - picomatch: 3.x - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10/fabde9004433983ba22fac2578c0b2835a498e66518df9d7994b1564a5f3f015ac974efaff02975b56966e01a684f600fe4cce0bb2f146aedec4dcf35d6c5906 - languageName: node - linkType: hard - "figures@npm:^3.0.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -4676,13 +4678,6 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^9.0.0": - version: 9.0.0 - resolution: "js-tokens@npm:9.0.0" - checksum: 10/65e7a55a1a18d61f1cf94bfd7704da870b74337fa08d4c58118e69a8b10225b5ad887ff3ae595d720301b0924811a9b0594c679621a85ecbac6e3aac8533c53b - languageName: node - linkType: hard - "js-yaml@npm:^4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" @@ -5860,13 +5855,6 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^4.0.2": - version: 4.0.2 - resolution: "picomatch@npm:4.0.2" - checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717 - languageName: node - linkType: hard - "pidtree@npm:^0.6.0, pidtree@npm:~0.6.0": version: 0.6.0 resolution: "pidtree@npm:0.6.0" @@ -6172,7 +6160,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.13.0, rollup@npm:^4.18.1, rollup@npm:^4.9.5": +"rollup@npm:^4.13.0, rollup@npm:^4.19.0, rollup@npm:^4.9.5": version: 4.19.0 resolution: "rollup@npm:4.19.0" dependencies: @@ -6663,15 +6651,6 @@ __metadata: languageName: node linkType: hard -"strip-literal@npm:^2.1.0": - version: 2.1.0 - resolution: "strip-literal@npm:2.1.0" - dependencies: - js-tokens: "npm:^9.0.0" - checksum: 10/21c813aa1e669944e7e2318c8c927939fb90b0c52f53f57282bfc3dd6e19d53f70004f1f1693e33e5e790ad5ef102b0fce2b243808229d1ce07ae71f326c0e82 - languageName: node - linkType: hard - "sucrase@npm:^3.35.0": version: 3.35.0 resolution: "sucrase@npm:3.35.0" @@ -6941,9 +6920,9 @@ __metadata: languageName: node linkType: hard -"tsup@npm:^8.2.1": - version: 8.2.1 - resolution: "tsup@npm:8.2.1" +"tsup@npm:^8.1.0, tsup@npm:^8.2.1": + version: 8.2.2 + resolution: "tsup@npm:8.2.2" dependencies: bundle-require: "npm:^5.0.0" cac: "npm:^6.7.14" @@ -6952,13 +6931,12 @@ __metadata: debug: "npm:^4.3.5" esbuild: "npm:^0.23.0" execa: "npm:^5.1.1" - fdir: "npm:^6.1.1" + globby: "npm:^11.1.0" joycon: "npm:^3.1.1" picocolors: "npm:^1.0.1" - picomatch: "npm:^4.0.2" postcss-load-config: "npm:^6.0.1" resolve-from: "npm:^5.0.0" - rollup: "npm:^4.18.1" + rollup: "npm:^4.19.0" source-map: "npm:0.8.0-beta.0" sucrase: "npm:^3.35.0" tree-kill: "npm:^1.2.2" @@ -6979,7 +6957,7 @@ __metadata: bin: tsup: dist/cli-default.js tsup-node: dist/cli-node.js - checksum: 10/0c87200e7962ab069d4f5c18d5509fee77286ff9a65c132d4da9730f2b1030e108b06ce3968d810991563ec0b1bb577e73cb6f15de35d217fe5fdc7895a12d72 + checksum: 10/f0d25756d4760d76f0d75118cd222791ad630ae2e89dfc3341d1e2d96b20614d37d1e8a0a541f3a342348cdfb0b24d3df2e2e3ba7590e462630be969803ee6c3 languageName: node linkType: hard @@ -7305,9 +7283,9 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:2.0.3": - version: 2.0.3 - resolution: "vite-node@npm:2.0.3" +"vite-node@npm:2.0.4": + version: 2.0.4 + resolution: "vite-node@npm:2.0.4" dependencies: cac: "npm:^6.7.14" debug: "npm:^4.3.5" @@ -7316,7 +7294,7 @@ __metadata: vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10/987c018c58ddcd084a51467eadb8dd75b55d31d39de1dec92dfd49f9ef115004f9a1c7f543f5f6709911e3080605bd50ada81cabb407030ec21b7baf6dd30476 + checksum: 10/27040a5d614fa315cc735867d7e6778640b2dcfb164e1a18d6a275b991a21e99ac6d720448b1b8de6e6d10b8169e79d0cb022807d537246b816f0260eb5f8b15 languageName: node linkType: hard @@ -7360,17 +7338,17 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^2.0.3": - version: 2.0.3 - resolution: "vitest@npm:2.0.3" +"vitest@npm:^2.0.2, vitest@npm:^2.0.3": + version: 2.0.4 + resolution: "vitest@npm:2.0.4" dependencies: "@ampproject/remapping": "npm:^2.3.0" - "@vitest/expect": "npm:2.0.3" - "@vitest/pretty-format": "npm:^2.0.3" - "@vitest/runner": "npm:2.0.3" - "@vitest/snapshot": "npm:2.0.3" - "@vitest/spy": "npm:2.0.3" - "@vitest/utils": "npm:2.0.3" + "@vitest/expect": "npm:2.0.4" + "@vitest/pretty-format": "npm:^2.0.4" + "@vitest/runner": "npm:2.0.4" + "@vitest/snapshot": "npm:2.0.4" + "@vitest/spy": "npm:2.0.4" + "@vitest/utils": "npm:2.0.4" chai: "npm:^5.1.1" debug: "npm:^4.3.5" execa: "npm:^8.0.1" @@ -7381,13 +7359,13 @@ __metadata: tinypool: "npm:^1.0.0" tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" - vite-node: "npm:2.0.3" - why-is-node-running: "npm:^2.2.2" + vite-node: "npm:2.0.4" + why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.0.3 - "@vitest/ui": 2.0.3 + "@vitest/browser": 2.0.4 + "@vitest/ui": 2.0.4 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -7405,7 +7383,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10/ef46775bad4c900c9db3187621ee092c203bcf50b8ac8321eed3105c8256d6781652e5f7968027062f306c0426b8f1d446cebd5d61b787c0af34499e40cf7746 + checksum: 10/01a173adbf40273adce5ff0ffd7b538fcc98286b15441651be4a3b9cc48748acf6cedb1f4966b4eff07ed91695847b9352591fd419c2da62181440bc6edf79ee languageName: node linkType: hard @@ -7542,15 +7520,15 @@ __metadata: languageName: node linkType: hard -"why-is-node-running@npm:^2.2.2": - version: 2.2.2 - resolution: "why-is-node-running@npm:2.2.2" +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" dependencies: siginfo: "npm:^2.0.0" stackback: "npm:0.0.2" bin: why-is-node-running: cli.js - checksum: 10/f3582e0337f4b25537d492b1d40f00b978ce04b1d1eeea8f310bfa8aae8a7d11d118d672e2f0760c164ce3753a620a70aa29ff3620e340197624940cf9c08615 + checksum: 10/0de6e6cd8f2f94a8b5ca44e84cf1751eadcac3ebedcdc6e5fbbe6c8011904afcbc1a2777c53496ec02ced7b81f2e7eda61e76bf8262a8bc3ceaa1f6040508051 languageName: node linkType: hard