From 69108d1557b0df1eefa1034499de683d39393747 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:52:06 +0000 Subject: [PATCH] M1: core path escaping, order-preserving index value encoding, errors Scaffold TypeScript/Vitest project, add DESIGN.md (brief) and CLAUDE.md. Property tests cover escape round-trips, separator-freedom and order preservation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE --- .gitignore | 4 + CLAUDE.md | 20 + DESIGN.md | 387 +++++++++ package-lock.json | 1680 ++++++++++++++++++++++++++++++++++++ package.json | 49 ++ src/core/encoding.ts | 177 ++++ src/core/path.ts | 109 +++ src/errors.ts | 47 + src/index.ts | 3 + test/core/encoding.test.ts | 72 ++ test/core/path.test.ts | 82 ++ tsconfig.build.json | 5 + tsconfig.json | 21 + vitest.config.ts | 9 + 14 files changed, 2665 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 DESIGN.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/core/encoding.ts create mode 100644 src/core/path.ts create mode 100644 src/errors.ts create mode 100644 src/index.ts create mode 100644 test/core/encoding.test.ts create mode 100644 test/core/path.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab899ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +coverage +*.log diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..72263e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# PotentialStorage + +Layered TypeScript storage library: KV → Events → Documents. The full design, milestones and +implementation decisions are in [DESIGN.md](./DESIGN.md) — read it before changing anything. +Record new decisions in DESIGN.md section 12. + +## Commands + +- `npm test` — run all tests (Vitest; includes `*.test-d.ts` type tests) +- `npm run typecheck` — `tsc --noEmit` over src, test and bench +- `npm run bench` — `vitest bench` +- `npm run build` — emit `dist/` + +## Rules + +- No runtime dependencies in `src/`. Dev dependencies only. +- Drivers stay flat and string-keyed; all path/index/escaping logic lives in the node classes. +- Self vs child dispatch is by `arguments.length`; a passed-but-`undefined`/`null` key throws. +- Multi-result reads are `AsyncIterable`; don't build large arrays internally. +- Keep tests green and commit per milestone. Ask the owner before pushing. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..ee353ac --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,387 @@ +# PotentialStorage — Design + +This document is the design brief agreed with the project owner (sections 0–11), followed by +implementation decisions made where the brief was silent (section 12) and benchmark baselines +(section 13). + +## 0. Setup and workflow + +- Repository: `git@git.harvmaster.com:Harvmaster/PotentialStorage.git` (SSH; self-hosted Git, not GitHub). Clone it. If you can't reach it, stop and tell the owner rather than starting a fresh repo elsewhere. +- Inspect what's already in the repo first. If it has tooling (package.json, tsconfig, test runner), keep it. If it's empty, set up: + - TypeScript (`strict: true`, `noUncheckedIndexedAccess: true`), ESM, Node ≥ 20 + - Vitest for tests and `vitest bench` for benchmarks; `expectTypeOf` for type tests + - Package name `potential-storage` unless one already exists + - **No runtime dependencies in the core.** Dev dependencies are fine (e.g. `fast-check` for property tests). +- Copy this brief into the repo as `DESIGN.md` and create a short `CLAUDE.md` pointing at it plus the build/test commands. +- Work milestone by milestone (section 9). Commit after each milestone with a clear message, and keep tests green. Ask the owner before pushing. + +## 1. Principles + +1. **Three layers**, each usable on its own: **KV** → **Event** (append-only, built on KV) → **Document** (Mongo-like, built on Events or native). +2. **Drivers vs stores.** Backends implement a tiny, flat, string-keyed *driver*. All path, child, index, escaping and typing logic lives once in the user-facing *node* classes, so every backend gets identical behaviour. +3. **One navigation primitive: `child(key)`.** There are no collection/document classes. Every node is a store over its direct children, and `child(key)` re-roots to that key. Navigation is synchronous and does no I/O. +4. **No methods on data.** Reads return plain values. +5. **Performance is a requirement.** Batch operations are the primitive; scans are ordered and streamed; nothing replays or scans more than it must (section 8). + +## 2. Paths and key encoding (`src/core/path.ts`) + +- A node's path is `readonly string[]` of **unescaped** segments. Number keys are converted with `String()`. +- The separator is a single character, configurable, default `/`. +- **Escaping** (per segment): percent-encode `%`, the separator, and a **leading** `~`. Escaped segments never contain a raw separator. Write `escapeSegment` / `unescapeSegment` with round-trip property tests. +- **Reserved segments** start with `~` (user segments can't, because a leading `~` is escaped): + - `~i` index entries + - `~u` unique-index claims + - `~e` event commits + - `~p` document projections root +- Each node precomputes in its constructor: + - `selfKey` = escaped segments joined by the separator (e.g. `users/123`) + - `prefix` = `selfKey + sep` (`''` for the root) + - No string work per call beyond `prefix + escapeSegment(key)`. +- Keys are ordered **lexicographically** (so `10` sorts before `9`). Document this. An order-preserving numeric key encoding is a possible later option, not part of v1. + +Example layout: + +``` +users/123 → KV value of users/123 +users/123/posts/p1 → KV value of a nested child +users/~i/byEmail/sada@x.io/123 → index entry (value: empty / true) +orders/o1/~e/000000000007 → event commit #7 of stream orders/o1 +~p/users/123 → document projection { state, version, commit } +``` + +## 3. Layer 1: KV + +### 3.1 Driver interface (`src/kv/driver.ts`) + +```ts +export interface ScanOptions { + gte?: string + lt?: string + limit?: number + reverse?: boolean + values?: boolean // when false/omitted, yield value as undefined +} + +export interface KVDriver { + getMany(keys: string[]): Promise<(unknown | undefined)[]> // same order as keys + setMany(entries: [key: string, value: unknown][]): Promise + deleteMany(keys: string[]): Promise + scan(opts: ScanOptions): AsyncIterable<[key: string, value: unknown]> // MUST be key-ordered + + // Optional capabilities. Stores feature-detect them and fall back where possible. + setIfAbsent?(entries: [string, unknown][]): Promise // true = written + batch?(ops: BatchOp[]): Promise // atomic multi-op write + deleteRange?(gte: string, lt: string): Promise +} + +export type BatchOp = + | { type: 'set'; key: string; value: unknown } + | { type: 'delete'; key: string } +``` + +- Values are passed through as JSON-serialisable values. Drivers that need bytes (HTTP, Redis, files) serialise internally via a pluggable `Codec` (default JSON). +- Provide an abstract `BaseKVDriver` with default implementations of `deleteRange` (scan + deleteMany) and helpers, so a minimal driver implements only the four required methods. +- **`InMemoryKVDriver`**: a sorted structure (sorted array with binary search is fine for v1; keep it behind an interface so it can become a B-tree). It implements every optional capability: `setIfAbsent`, atomic `batch` and `deleteRange`. +- Write a reusable **driver conformance suite** `runKVDriverConformance(name, makeDriver)` covering ordering, `gte`/`lt` bounds, reverse, limit, `values` on/off, `getMany` order with missing keys, and each optional capability when present. + +### 3.2 `KVStorage` / `KVNode` (`src/kv/node.ts`) + +```ts +const db = new KVStorage(driver, { separator: '/', indexes: { ... } }) // db is the root KVNode +const users = db.child('users') +const user = users.child('123') +``` + +API of every `KVNode` (types in section 7): + +| Method | Meaning | +|---|---| +| `child(key)` | Sync. Returns a node at `path + key`. | +| `as()` | Type assertion for untyped use; returns the same node retyped. | +| `get()` / `get(key)` | Own value at `selfKey` / value at child key. | +| `getMany(keys)` | Batch child read, same order, `undefined` for missing. | +| `set(value)` / `set(key, value)` | Write own value / child value. | +| `setMany(entries)` | Batch child write. | +| `delete()` / `delete(key)` | Delete own value / child value only (not descendants). | +| `deleteMany(keys)` | Batch child delete. | +| `deleteTree()` / `deleteTree(key)` | Delete own value **and all descendants** (uses `deleteRange` if available). | +| `keys(opts?)` | `AsyncIterable` of **direct** child keys (unescaped). Options: `limit`, `after`, `reverse`. | +| `entries(opts?)` | `AsyncIterable<[key, value]>` of direct children that have an own value. | +| `tree({ depth })` | Assemble an object from descendant keys up to `depth` (for field-per-key style). Bounded and explicit; never the default. | +| `findBy(index, value \| { gte?, lt? }, opts?)` | Index lookup over direct children (section 3.3). | + +**Arity dispatch (important):** +- Self vs child is decided by `arguments.length`, never by checking for `undefined`. +- If a key argument is passed but is `undefined` or `null`, throw a `TypeError` (this prevents `users.get(maybeId)` silently reading self). +- The root node (`path = []`) has no own value: `get()`, `set(v)` and `delete()` on root throw. +- Implement one private `#resolve(args)` that every method uses. + +**Direct-children listing (`keys`)** uses a skip-scan: +1. Scan `gte: prefix`, `lt: prefix + succ(sep)`. +2. For each key, take the first segment after `prefix`. Skip reserved (`~…`) segments. Yield the segment (unescaped) if it differs from the previous one. +3. When the next key is a descendant of the segment just yielded, re-seek the driver to `gte: prefix + escSeg + succ(sep)` (where `succ(c)` is the next char code) instead of iterating through the subtree. Keep using the same iterator while keys are siblings. + +Listing must be O(direct children × seek cost), not O(subtree). A child that has no own value but has descendants **is** listed. + +### 3.3 Indexes (`src/kv/indexes.ts`) + +Indexes are declared on **path patterns** in the `KVStorage` constructor. A pattern names a parent node, and its index applies to that node's direct children's values: + +```ts +new KVStorage(driver, { + indexes: { + 'users': { byEmail: (u: User) => u.email }, + 'users/*/posts': { byDate: (p: Post) => p.createdAt, byTag: (p: Post) => p.tags }, + }, +}) +``` + +- An index function returns `IndexValue | IndexValue[] | undefined`, where `IndexValue = string | number | boolean | null`. An array means multi-entry; `undefined` means not indexed. +- Compile patterns into a **segment trie** (`*` matches one segment). A node resolves its index set **once, at construction**, in O(depth). +- Entry key: `~i///` with a `true` value. +- **`encodeIndexValue` must be order-preserving and separator-safe:** + - Type tag first, so values order by type: `null` < `false` < `true` < numbers < strings. + - Numbers use an order-preserving float64 encoding (flip the sign bit for positives and all bits for negatives, then fixed-width hex). + - Strings are escaped like segments. + - This enables `findBy(index, { gte, lt })` range scans. +- **Writes to an indexed node:** + 1. Read the old value(s) (`getMany`) and compute the diff of index entries. + 2. If the driver has `batch`, write the value and the index adds/removes atomically. + 3. Otherwise, write index **adds first**, then the value, then index **removes**. Treat index entries as hints: `findBy` re-reads each value, re-checks the index function, and lazily deletes stale entries (read-repair). +- Non-indexed nodes must **not** pay the read-before-write cost. + +## 4. Layer 2: Events (`src/events/`) + +An append-only log per stream. A stream is identified by a node path. Same `child()` navigation as KV. + +```ts +const events = new EventStorage(kv) // takes a KVStorage (or a KVNode as root) +const orders = events.child('orders') + +await orders.append('o1', [{ type: 'created', total: 40 }], { expectedVersion: 0 }) +await orders.child('o1').append([{ type: 'paid' }]) // self form +for await (const e of orders.read('o1', { from: 1 })) { /* { version, commit, at, data } */ } +await orders.version('o1') +for await (const id of orders.keys()) { } // child streams +orders.child('o1').child('shipments') +``` + +- `EventStorage` is the abstract interface. `KVEventStorage` is the KV-backed implementation. Leave room for native backends (EventStoreDB, Kafka) implementing the same interface. +- **Arity:** `append(events, opts?)` vs `append(key, events, opts?)`. Dispatch on `Array.isArray(args[0])`, plus the undefined-key rule from 3.2. `read`, `version` and `keys` follow the 3.2 rules. +- **Versions:** an empty stream has version 0, and the first event has version 1. `expectedVersion: number | 'any'` (default `'any'`). +- **Storage: one key per commit, densely numbered.** + - Key: `/~e/` + - Value: `{ firstVersion, at, events: E[] }` + - The commit sequence (not the event version) is the key so that **any** stale writer collides with the existing key. Keying by version would let a stale writer create overlapping version ranges. +- **Append:** + 1. Get the head `{ commitSeq, version }` (from the cache, or a reverse scan with `limit: 1`). + 2. If `expectedVersion` is a number and doesn't match the head, throw `ConcurrencyError`. + 3. Call `setIfAbsent(nextCommitKey)`. + 4. On `false` (collision): refresh the head. If `expectedVersion` was given, throw `ConcurrencyError`; otherwise retry up to 5 times. + 5. Without `setIfAbsent`, fall back to a per-stream in-process mutex. Document that this mode is single-writer only. +- **Head cache:** a bounded LRU keyed by stream path. Update it on successful append; invalidate it on collision. +- **`read({ from, to, reverse, limit })`** streams events with metadata. To start at version `from`, locate the containing commit by binary search over commit numbers using `getMany` probes (O(log commits) round trips), then forward-scan. +- There is no update or delete. Keys inside `~e` are never rewritten. + +## 5. Layer 3: Documents (`src/documents/`) + +`DocumentStorage` is the interface. There are two implementations: +- `EventSourcedDocumentStorage(events, { projections?: KVNode })` is the v1 target. Projections default to the `~p` root on the same KV. +- `MongoDocumentStorage(client)` is a later milestone (section 9). + +```ts +const docs = new EventSourcedDocumentStorage(events, { indexes: { 'users': [{ fields: ['email'], unique: true }, { fields: ['age'] }] } }) +const users = docs.child('users') + +const id = await users.insert({ name: 'Ada', age: 36 }) // generates a time-sortable id +await users.set('123', { name: 'Ada', age: 36 }) +await users.update('123', { $set: { name: 'Ada L.' }, $inc: { logins: 1 } }) +await users.child('123').update({ $unset: { nickname: true } }) // self form +await users.get('123') +await users.delete('123') +for await (const { key, data } of users.find({ age: { $gte: 18 } }, { sort: { age: 1 }, limit: 20 })) { } +await users.findOne({ email: 'ada@x.io' }) +await users.history('123') // event-sourced only +await users.getAt('123', 17) // state as of version 17 +users.child('123').child('posts') // nested docs, same as KV +``` + +### 5.1 Events and projections + +- The doc at path `P` has its event stream at `P` in `EventStorage`. Its projection is at `~p/P` in the projections KV: `{ state, version }` (and `deleted: true` for tombstones). +- Event types: `{ op: 'set', doc }`, `{ op: 'patch', set?: Record, unset?: path[] }`, `{ op: 'delete' }`. +- **Store resolved deltas, not operators.** For example, `$inc` resolves to `{ set: { logins: 6 } }`, so replay is deterministic. +- Supported update operators in v1: `$set`, `$unset`, `$inc`, `$push`, `$pull`, `$addToSet`, with dot-path support. + +**Write path:** +1. Read the projection. +2. Compute the new state and patch. +3. `append` with `expectedVersion = projection.version`. +4. Write the projection and index entries (atomic `batch` if available). +5. On `ConcurrencyError`: catch up the projection from the stream, recompute, and retry (bounded). + +**Reads** are a single projection `get`. Never replay on read. + +**Crash between append and projection write:** +- The projection lags the stream. The next write's `expectedVersion` fails, which triggers catch-up, so the system self-heals. +- Provide `get(key, { consistent: true })` that checks the stream head and catches up first. +- Provide a `catchUp(node?)` maintenance method to repair a subtree. +- `insert` ids are time-sortable (ULID-like). Implement a small generator, with no dependency. + +### 5.2 Queries and document indexes + +- Filter subset: + - `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists` + - `$and`, `$or`, `$not` + - implicit equality and dot paths +- Options: `sort`, `limit`, `skip`, `projection` (fields). +- `find` on a node queries that node's **direct children**. +- Indexes are declared per path pattern (same trie mechanism as 3.3). + - Each index has `fields` (compound allowed); entries go under `~p//~i////…/`, using `encodeIndexValue`. + - `unique: true` additionally claims `~u//` via `setIfAbsent`. Throw at construction if the driver lacks `setIfAbsent`. +- **Planner:** + - Pick the index that covers the most leading equality fields, then at most one range field. + - Use the index order for `sort` when it matches. + - Otherwise do a full scan of children projections, with a one-time dev-mode `console.warn` naming the filter. + +## 6. Shared conventions + +- Every layer uses the same navigation (`child`, `as`, `keys`) and the same self/child arity rules. +- All multi-result reads are `AsyncIterable` (streamed, cancellable). Never build large arrays internally. +- Errors are typed classes exported from `src/errors.ts`: `ConcurrencyError`, `UniqueConstraintError`, `CapabilityError`, `InvalidKeyError`. +- Public exports go through `src/index.ts`, with subpath exports per layer if convenient. + +## 7. Types (`src/types/schema.ts`) + +Each node is typed by a single **Spec** that describes its own value and its children by key: + +```ts +export type Spec = { value?: unknown; children?: Record } +export type AnySpec = { value: unknown; children: Record } // untyped default + +export type ValueOf = S extends { value: infer V } ? V : never +export type ChildrenOf = S extends { children: infer C } ? C : {} +export type KeyOf = Extract, string | number> +export type ChildOf> = ChildrenOf[K] + +// Helpers +export type Many = {}> = + { children: { [id: string]: { value: V; children: C } } } +export type Fields = { [K in keyof T & string]: { value: T[K] } } +``` + +Node signatures: + +```ts +class KVNode { + child>(key: K): KVNode> + get(): Promise | undefined> + get>(key: K): Promise> | undefined> + // set/delete/getMany/etc. follow the same pattern +} +``` + +Example to cover with `expectTypeOf` tests: + +```ts +type User = { username: string; age: number } +type Post = { title: string; createdAt: string } +type Schema = { children: { + users: Many }> + profile: { children: { [id: string]: { children: Fields } } } // field-per-key style +} } + +const db = new KVStorage(driver) +db.child('users').get('123') // User | undefined +db.child('users').child('123').get() // User | undefined (same key) +db.child('users').child('123').child('posts') // KVNode> +db.child('users').child('123').child('postz') // ✗ type error +db.child('profile').child('9').get('age') // number | undefined +db.child('profile').child('9').get('agee') // ✗ type error +``` + +Notes: +- **Named levels** check keys at compile time; **dynamic `[id: string]` levels** accept any string. +- One node's children can't mix an index signature with named keys (a TS restriction), so schemas alternate between dynamic and named levels. That alternation exists only in types; the runtime API is just `child()`. +- `EventStorage` and `DocumentStorage` nodes use the same Spec machinery (the value type is the event type or the document type respectively). + +## 8. Performance requirements + +- Single-key ops: exactly one driver call (no hidden reads), except indexed writes (one `getMany` before the write) and document writes (projection read plus append plus projection write). +- `*Many` ops: one driver call each (plus index work), never N calls. +- `keys()`: skip-scan, O(direct children) seeks. +- Event head lookup: cached; cold lookup is a single reverse scan with `limit: 1`. +- Document `get`: one driver `get`. +- Add `vitest bench` benchmarks for: + - KV `get`/`set`/`getMany` at 10k/100k keys + - `keys()` on a node with 1k children each having 100 descendants + - event append and read + - document `get`/`update`/indexed `find` vs full-scan `find` +- Record baseline numbers in `DESIGN.md`. + +## 9. Milestones + +1. **Core:** `path.ts` (escape, unescape, encode, `succ`), `encodeIndexValue`, errors. Property tests for round-trips and order preservation. +2. **KV driver:** `KVDriver`, `BaseKVDriver`, `InMemoryKVDriver`, conformance suite. +3. **KV node:** full API from 3.2, including arity rules, skip-scan `keys`, `deleteTree`, `tree`. Add a model-based property test: random ops vs a reference `Map`. +4. **KV indexes:** pattern trie, atomic and non-atomic write paths, read-repair, range `findBy`. +5. **Events:** `KVEventStorage` with commit keys, `setIfAbsent` and mutex modes, head cache, binary-search `read({ from })`, concurrency tests (simulated interleaving with two storage instances on one driver). +6. **Documents:** `EventSourcedDocumentStorage`, update operators, projections, catch-up, queries, planner, unique indexes, `history` and `getAt`. +7. **Types:** Spec machinery wired through all three layers, plus `expectTypeOf` tests from section 7. +8. **HTTP driver:** + - A `createKVHttpHandler(driver)` (framework-agnostic `Request → Response`) plus an `HttpKVDriver` client. + - Endpoints: `POST /getMany`, `POST /setMany`, `POST /deleteMany`, `POST /batch`, `POST /setIfAbsent`, `GET /scan?gte<&limit&reverse&values` (NDJSON stream). + - It must pass the conformance suite end-to-end. +9. **Benchmarks** and a `DESIGN.md` update with results. +10. **(Later, ask first)** `MongoDocumentStorage`: + - Path pattern maps to a collection (e.g. `users/*/posts` → `users.posts`), with a `_parent` field and a compound index `(_parent, _id)`. + - Native indexes map from the same index declarations. + - It should pass the document test suite where semantics allow (no `history`/`getAt`). + +## 10. Decisions already made (don't revisit without asking) + +- `child()`-only navigation; no collection/doc classes. +- `get()` with no argument reads the node's **own key**. It never assembles the subtree; `tree()` does that explicitly. +- A node's own value and its children are independent keys. +- Self/child dispatch by arity; a passed-but-undefined key throws. +- Indexes are declared on path patterns at construction. +- Events use one key per commit, densely numbered. +- Documents store resolved deltas, and reads hit projections only. +- Keys sort lexicographically. +- Values are JSON-serialisable; byte drivers use a codec. + +## 11. Open questions to raise with the owner (don't block on them) + +- Order-preserving numeric **keys** (not just index values)? +- Should multi-writer safety be **required** (a hard error without `setIfAbsent`) rather than a documented single-writer fallback? +- The hard-delete story for immutable event logs (compaction or crypto-shredding) for privacy deletion requests. + +## 12. Implementation decisions + +Decisions taken where the brief was silent. Each is the simplest option consistent with sections 1–10. + +### M1 — Core + +- **Separator** must be a single printable ASCII character other than `%` and `~`. +- **Empty keys are rejected** (`InvalidKeyError`), as are `NaN`/`±Infinity` number keys. An empty segment + would produce `a//b`, which is ambiguous with the subtree layout. +- **Separator escaping** uses `%` + two uppercase hex digits (`/` → `%2F`, `%` → `%25`, leading `~` → `%7E`). +- **`InvalidKeyError` extends `TypeError`**, so "passed-but-undefined key throws a `TypeError`" (3.2) and + the typed error class (6) are the same throw. +- **Index string encoding deviates from "escaped like segments".** Percent-escaping is not + order-preserving (`"a/"` → `"a%2F"` sorts below `"a&"`, and `"a"` + sep sorts above `"a!"`), which + would break `findBy` range scans. Instead every character the index encoder emits sorts strictly + above the separator: code units `<= succ(sep)` are written as `succ(sep)` followed by + `chr(succ(sep) + 1 + c)`. This is monotone and prefix-safe, so `a < b ⇔ enc(a)+sep+… < enc(b)+sep+…` + exactly (property-tested). Encodings are still separator-free. +- **Type tags and number digits**: tags are the first five digits of the digit alphabet; numbers use + 16 digits of `0-9a-f` when the separator sorts below `'0'` (e.g. `/`), otherwise the 16 characters + just above the separator. `-0` is stored as `0`; `NaN` cannot be indexed. +- **Ordering** of strings is by UTF-16 code unit (JS `<`). Drivers that compare UTF-8 bytes agree + except for astral-plane characters vs. U+E000–U+FFFF; noted as a driver-author caveat. + + + +## 13. Benchmark baselines + +_Filled in at milestone 9._ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..889c03a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1680 @@ +{ + "name": "potential-storage", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "potential-storage", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.20.5", + "fast-check": "^4.10.2", + "typescript": "^5.9.3", + "vitest": "^3.2.7" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.65.0.tgz", + "integrity": "sha512-t2AnVba25baFdq7gczzS13JyVcdubLVbFX1X04mxrggDrTQPUXNv3XM0k9H1hr5tqvg7AVvtQ720QHAj0iYqhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.65.0.tgz", + "integrity": "sha512-dNONUaEwPGhl/nuO2KV8VABWlcjYyU6KtZGN8dQv5OS5JU7JSCxPijVNKXbRJd0iuTzTiJ4TjPt/4bbvDRNiMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.65.0.tgz", + "integrity": "sha512-zWQqUH+9zavr2Bxuzfpuv2SIF3X31cr58TvSQC9o1GhpOvkqRwr60GkZdQcudyT/BEgdpdSafjgd2I13MbTL7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.65.0.tgz", + "integrity": "sha512-RQYreBFWtO4//hIuOaZkYUnF/lPU4lz35dveDimDshWCJvVisVtJ/shmT9bkXK8wFJY+FJF89X6F3Cq1qI3LCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.65.0.tgz", + "integrity": "sha512-A0NfMj6Cuh/9BE5k3awJKv6HoxFVXIcLAb6SZqAMoxUHPA1ZNNK4RsGuUPD/vL7nEAm9LG+ZzXWWTrnC0XtkpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.65.0.tgz", + "integrity": "sha512-BI6/H7q3aP0I7S7GOrjyxTjgEJlSnovXE38+bdxBpuKN6z+d//uF+T+VQ3Q7ltJL/71p17ufartARX9Ja5gOGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.65.0.tgz", + "integrity": "sha512-W1vUprf09RWaVWnsFP1vlUkEWJzTZlpObOEAYZXt9Fq0UVAOYEkeGr7wBX7McKqqIQskXdBnpiOMgwPujordgA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.65.0.tgz", + "integrity": "sha512-s8QTcZWgrl9haFzEGIqyE5BWyo89pFDGLMBFZ752WYxk8DJwNU/O1X6pZgPw829d9/sm6CCHyrMP3aurt76p1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.65.0.tgz", + "integrity": "sha512-ke1f6dLRzkqveyjjF2fkEwQYZvxHtNNwjegY8nJrwcO0Bdqr8CzFO/pKE0Bns13cx0qz2Ba2Y0Hs3dSwMdnHxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.65.0.tgz", + "integrity": "sha512-926SMeUcy4y18Rz/w2aJiUKOnyAnm6czg72bXkQEaeemsll+Wr0hnsjb2BGRpHvWgwAMYy5ACJCotbvfLCfj0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.65.0.tgz", + "integrity": "sha512-6T7JNRP1hc4hqF2dUsEoVLOt9K8tmODoCLldtQRYnrddGGQY8Gqe2pM/qMMUo7EMowpP7oa2N95k0/8sYK/ipw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.65.0.tgz", + "integrity": "sha512-SImGFl1Je5kpyAgOCPONVT7h6ASV8mAUwGRjY7eZvVjPg//PfHOXgI+P241RF/+HqsA4PP3qD527ysWFKNQyhQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.65.0.tgz", + "integrity": "sha512-XeDTbmPF1Yu5asfXGPrMkWoxTDhgryQbmw5Q1+R582O51VNCk23J6f2pDrsAp5YkneBVBwtExx6ZGOb9D2aQ6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.65.0.tgz", + "integrity": "sha512-VS55JUI/1nsljmV/jfqNkLZXFw8uMO8nNlfoHmSevNf6fO3neuhyDvx9dv3c/Ytt8r/431SL8Qax1dpYvOMZTg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.65.0.tgz", + "integrity": "sha512-Vblw84TbX7RS4la1Y4+yQ7aore/oSyaNtFbppuBr5Vs/4LfBeXKq9dIfoExw6zOvRERqd2rRAjnZRaMhH/0x5w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.65.0.tgz", + "integrity": "sha512-7+gHjtONvKgwR9InTKFaLq53f6YzBJkgeMRBENdWTsQr+spahGqQm8HF5aXpq8i4JaRrOqYyv6Zn1WltDTxaZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.65.0.tgz", + "integrity": "sha512-KvNDC0J6s4vVHhtJ6KrDXU0Er0au/hmRIBq/+1maCV8TseGupeKizq0bWcrbp2HE1T8xoknvN0Q/KZ/YF0qX3w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.65.0.tgz", + "integrity": "sha512-GExdD15WPvu/x/drELjVam7vX8EsPgvTMOa/iJL/p/4m/oOqzbb7JXZg8fXR4hlXtg/sF2zyTZ3bR0AEtEMlhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.65.0.tgz", + "integrity": "sha512-8sT9PemIZfIMED4Vd4KGoOXVDj5CMSWNcGdbheRUx9TYS03yOWdT2Q0lXPyEyNBB9YTilwewIQKNIBk6Nxi0Kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.65.0.tgz", + "integrity": "sha512-pO6Ehi0jo5yE0R6PltKhJhv9mlIiyzBJk5N5NUF2OF6xtVKI+1bdXxj/rb0jGXy3hck1ELVT0QPY3tzkAm360w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.65.0.tgz", + "integrity": "sha512-vo7N3/+cjjPDYr8vtxC+qSNgWa9sDvJ7g8N8R5SWqFY4WiNH1ULcAU70SKVyHeiIZOYYwNP9pTipmhN5sl1NPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.65.0.tgz", + "integrity": "sha512-ArwvgFAbjKcDM7qqotZ4OVYf+UMT9xH2YzkCzK4+AuZSDihMlz1Ekoe4INwBVXzn2mnJzuIYOcSx07iTBijGRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.65.0.tgz", + "integrity": "sha512-QGMmK4/cxKE/36FoLMSHQBCyymv/eFTnsCvvsBF0rgmOvVbvX78VWZRerL3dHa77f4pM6Xcc2iGDhJ10w/svKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.65.0.tgz", + "integrity": "sha512-XfKYFWHIPs4HPODIMsGF1IKlS3H1e3WokUZIgpmn4+uNnZBlnRDDHEylNyazbQXwhMKZre68OjtRYbKqb5+j8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.65.0.tgz", + "integrity": "sha512-wPWi++K4Bs1VR4XihOhfGi4IGX6Cub7FlerTf8X2v+2BtDHddgSqWCXwGD+h2yAe4/niIhNDwAltioOC+wEogQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-check": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz", + "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.65.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.65.0.tgz", + "integrity": "sha512-QVCvQUTGDN6JRwi3jVGCY3H4aTHvi9rX0LyoBMkGUvdh/Z6O51CzcLz85BWZaTCDGhl6ySGqYZs+xI1jmPqGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.65.0", + "@rollup/rollup-android-arm64": "4.65.0", + "@rollup/rollup-darwin-arm64": "4.65.0", + "@rollup/rollup-darwin-x64": "4.65.0", + "@rollup/rollup-freebsd-arm64": "4.65.0", + "@rollup/rollup-freebsd-x64": "4.65.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.65.0", + "@rollup/rollup-linux-arm-musleabihf": "4.65.0", + "@rollup/rollup-linux-arm64-gnu": "4.65.0", + "@rollup/rollup-linux-arm64-musl": "4.65.0", + "@rollup/rollup-linux-loong64-gnu": "4.65.0", + "@rollup/rollup-linux-loong64-musl": "4.65.0", + "@rollup/rollup-linux-ppc64-gnu": "4.65.0", + "@rollup/rollup-linux-ppc64-musl": "4.65.0", + "@rollup/rollup-linux-riscv64-gnu": "4.65.0", + "@rollup/rollup-linux-riscv64-musl": "4.65.0", + "@rollup/rollup-linux-s390x-gnu": "4.65.0", + "@rollup/rollup-linux-x64-gnu": "4.65.0", + "@rollup/rollup-linux-x64-musl": "4.65.0", + "@rollup/rollup-openbsd-x64": "4.65.0", + "@rollup/rollup-openharmony-arm64": "4.65.0", + "@rollup/rollup-win32-arm64-msvc": "4.65.0", + "@rollup/rollup-win32-ia32-msvc": "4.65.0", + "@rollup/rollup-win32-x64-gnu": "4.65.0", + "@rollup/rollup-win32-x64-msvc": "4.65.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8287a98 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "potential-storage", + "version": "0.1.0", + "description": "Layered TypeScript storage: KV -> Events -> Documents", + "type": "module", + "engines": { + "node": ">=20" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./kv": { + "types": "./dist/kv/index.d.ts", + "import": "./dist/kv/index.js" + }, + "./events": { + "types": "./dist/events/index.d.ts", + "import": "./dist/events/index.js" + }, + "./documents": { + "types": "./dist/documents/index.d.ts", + "import": "./dist/documents/index.js" + }, + "./http": { + "types": "./dist/http/index.d.ts", + "import": "./dist/http/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "bench": "vitest bench --run" + }, + "devDependencies": { + "@types/node": "^22.20.5", + "fast-check": "^4.10.2", + "typescript": "^5.9.3", + "vitest": "^3.2.7" + } +} diff --git a/src/core/encoding.ts b/src/core/encoding.ts new file mode 100644 index 0000000..8a35961 --- /dev/null +++ b/src/core/encoding.ts @@ -0,0 +1,177 @@ +import { DEFAULT_SEPARATOR, succ } from './path.js' + +export type IndexValue = string | number | boolean | null + +/** + * Order-preserving, separator-safe encoding of index values. + * + * Every character the encoder emits sorts strictly above the separator. That makes encodings + * prefix-safe inside keys: `enc(a) + sep + …` sorts below `enc(b)` whenever `enc(a)` is a proper + * prefix of `enc(b)`, so `a < b ⇔ key(a, …) < key(b, …)` and range scans over index entries are exact. + * + * Layout: one type-tag character, then a body. + * - Type tags order `null < false < true < number < string`. + * - Numbers: IEEE-754 float64, big-endian; positive values have the sign bit flipped, negative + * values have all bits flipped; written as 16 fixed-width "hex" digits. `-0` is normalised to `0`; + * `NaN` is rejected. + * - Strings: each code unit `c` with `c <= E` (where `E = succ(sep)`) is written as `E` followed by + * `chr(E + 1 + c)`; every other code unit is written as-is. This is monotone, so order is kept. + * + * The 16 digits are `0-9a-f` when the separator sorts below `'0'` (e.g. `/`), otherwise the 16 + * characters immediately after the separator. + */ +export interface IndexValueCodec { + readonly sep: string + encode(value: IndexValue): string + decode(encoded: string): IndexValue +} + +const codecs = new Map() + +export function indexValueCodec(sep: string = DEFAULT_SEPARATOR): IndexValueCodec { + let codec = codecs.get(sep) + if (!codec) { + codec = makeCodec(sep) + codecs.set(sep, codec) + } + return codec +} + +export function encodeIndexValue(value: IndexValue, sep: string = DEFAULT_SEPARATOR): string { + return indexValueCodec(sep).encode(value) +} + +export function decodeIndexValue(encoded: string, sep: string = DEFAULT_SEPARATOR): IndexValue { + return indexValueCodec(sep).decode(encoded) +} + +/** Encodes a compound value as `enc(v1) + sep + enc(v2) + …`. */ +export function encodeIndexValues(values: readonly IndexValue[], sep: string = DEFAULT_SEPARATOR): string { + const codec = indexValueCodec(sep) + let out = '' + for (let i = 0; i < values.length; i++) { + if (i > 0) out += sep + out += codec.encode(values[i]!) + } + return out +} + +export function isIndexValue(v: unknown): v is IndexValue { + return v === null || typeof v === 'string' || typeof v === 'boolean' || (typeof v === 'number' && !Number.isNaN(v)) +} + +function makeCodec(sep: string): IndexValueCodec { + const sepCode = sep.charCodeAt(0) + const digits = + sepCode < 0x30 ? '0123456789abcdef' : Array.from({ length: 16 }, (_, i) => String.fromCharCode(sepCode + 1 + i)).join('') + const digitValue = new Map([...digits].map((d, i) => [d, i])) + const TAG_NULL = digits[0]! + const TAG_FALSE = digits[1]! + const TAG_TRUE = digits[2]! + const TAG_NUMBER = digits[3]! + const TAG_STRING = digits[4]! + const esc = succ(sep) + const escCode = esc.charCodeAt(0) + + const view = new DataView(new ArrayBuffer(8)) + + function hex32(n: number): string { + let out = '' + for (let shift = 28; shift >= 0; shift -= 4) out += digits[(n >>> shift) & 0xf] + return out + } + + function encodeNumber(n: number): string { + if (Number.isNaN(n)) throw new TypeError('NaN cannot be indexed') + view.setFloat64(0, n === 0 ? 0 : n) // normalise -0 + let hi = view.getUint32(0) + let lo = view.getUint32(4) + if (hi & 0x80000000) { + hi = ~hi >>> 0 + lo = ~lo >>> 0 + } else { + hi = (hi ^ 0x80000000) >>> 0 + } + return hex32(hi) + hex32(lo) + } + + function decodeNumber(body: string): number { + if (body.length !== 16) throw new TypeError(`Bad encoded number ${JSON.stringify(body)}`) + let hi = 0 + let lo = 0 + for (let i = 0; i < 8; i++) hi = (hi << 4) | digitValue.get(body[i]!)! + for (let i = 8; i < 16; i++) lo = (lo << 4) | digitValue.get(body[i]!)! + hi >>>= 0 + lo >>>= 0 + if (hi & 0x80000000) { + hi = (hi ^ 0x80000000) >>> 0 + } else { + hi = ~hi >>> 0 + lo = ~lo >>> 0 + } + view.setUint32(0, hi) + view.setUint32(4, lo) + return view.getFloat64(0) + } + + function encodeString(s: string): string { + let out = '' + let start = 0 + for (let i = 0; i < s.length; i++) { + const c = s.charCodeAt(i) + if (c <= escCode) { + out += s.slice(start, i) + esc + String.fromCharCode(escCode + 1 + c) + start = i + 1 + } + } + return start === 0 ? s : out + s.slice(start) + } + + function decodeString(body: string): string { + if (body.indexOf(esc) === -1) return body + let out = '' + for (let i = 0; i < body.length; i++) { + const c = body[i]! + if (c === esc) { + const next = body.charCodeAt(++i) + out += String.fromCharCode(next - escCode - 1) + } else out += c + } + return out + } + + return { + sep, + encode(value) { + if (value === null) return TAG_NULL + switch (typeof value) { + case 'boolean': + return value ? TAG_TRUE : TAG_FALSE + case 'number': + return TAG_NUMBER + encodeNumber(value) + case 'string': + return TAG_STRING + encodeString(value) + default: + throw new TypeError(`Cannot index value of type ${typeof value}`) + } + }, + decode(encoded) { + const tag = encoded[0] + const body = encoded.slice(1) + switch (tag) { + case TAG_NULL: + return null + case TAG_FALSE: + return false + case TAG_TRUE: + return true + case TAG_NUMBER: + return decodeNumber(body) + case TAG_STRING: + return decodeString(body) + default: + throw new TypeError(`Bad encoded index value ${JSON.stringify(encoded)}`) + } + }, + } +} diff --git a/src/core/path.ts b/src/core/path.ts new file mode 100644 index 0000000..bceeef9 --- /dev/null +++ b/src/core/path.ts @@ -0,0 +1,109 @@ +import { InvalidKeyError } from '../errors.js' + +export const DEFAULT_SEPARATOR = '/' + +/** Reserved segments. User segments can never start with `~` (a leading `~` is escaped). */ +export const RESERVED = { + index: '~i', + unique: '~u', + events: '~e', + projections: '~p', +} as const + +export type Key = string | number + +/** The character immediately after `c` (by UTF-16 code unit). */ +export function succ(c: string): string { + return String.fromCharCode(c.charCodeAt(0) + 1) +} + +export function validateSeparator(sep: string): void { + if (sep.length !== 1) throw new TypeError(`Separator must be a single character, got ${JSON.stringify(sep)}`) + const code = sep.charCodeAt(0) + if (code <= 0x20 || code >= 0x7f || sep === '%' || sep === '~') { + throw new TypeError(`Separator must be printable ASCII other than "%" and "~", got ${JSON.stringify(sep)}`) + } +} + +function hex2(code: number): string { + return '%' + code.toString(16).toUpperCase().padStart(2, '0') +} + +/** Converts a user key to a segment string, rejecting `undefined`/`null`/empty. */ +export function toSegment(key: unknown): string { + if (key === undefined || key === null) throw new InvalidKeyError(`Key must not be ${key}`) + if (typeof key === 'number') { + if (!Number.isFinite(key)) throw new InvalidKeyError(`Numeric key must be finite, got ${key}`) + return String(key) + } + if (typeof key !== 'string') throw new InvalidKeyError(`Key must be a string or number, got ${typeof key}`) + if (key === '') throw new InvalidKeyError('Key must not be empty') + return key +} + +/** + * Escapes one path segment: percent-encodes `%`, the separator, and a leading `~`. + * The result never contains a raw separator and never starts with `~`. + */ +export function escapeSegment(segment: string, sep: string = DEFAULT_SEPARATOR): string { + // Fast path: nothing to escape (the overwhelmingly common case). + if (segment.indexOf('%') === -1 && segment.indexOf(sep) === -1 && segment.charCodeAt(0) !== 0x7e) return segment + let out = '' + for (let i = 0; i < segment.length; i++) { + const c = segment[i]! + if (c === '%' || c === sep || (i === 0 && c === '~')) out += hex2(c.charCodeAt(0)) + else out += c + } + return out +} + +export function unescapeSegment(escaped: string): string { + if (escaped.indexOf('%') === -1) return escaped + return escaped.replace(/%([0-9A-Fa-f]{2})/g, (_, h: string) => String.fromCharCode(parseInt(h, 16))) +} + +export function isReservedSegment(escaped: string): boolean { + return escaped.charCodeAt(0) === 0x7e // '~' +} + +/** Joins unescaped segments into an escaped key. */ +export function joinPath(path: readonly string[], sep: string = DEFAULT_SEPARATOR): string { + let out = '' + for (let i = 0; i < path.length; i++) { + if (i > 0) out += sep + out += escapeSegment(path[i]!, sep) + } + return out +} + +/** Splits an escaped key into unescaped segments. */ +export function splitPath(key: string, sep: string = DEFAULT_SEPARATOR): string[] { + if (key === '') return [] + return key.split(sep).map(unescapeSegment) +} + +/** + * Precomputed key strings for a node path. + * - `selfKey`: escaped segments joined by the separator (`''` for the root) + * - `prefix`: `selfKey + sep` (`''` for the root) — every descendant key starts with it + * - `end`: exclusive upper bound of the subtree range (`selfKey + succ(sep)`; `undefined` for the root) + */ +export interface PathKeys { + readonly path: readonly string[] + readonly sep: string + readonly selfKey: string + readonly prefix: string + readonly end: string | undefined +} + +export function pathKeys(path: readonly string[], sep: string): PathKeys { + const selfKey = joinPath(path, sep) + const root = path.length === 0 + return { + path, + sep, + selfKey, + prefix: root ? '' : selfKey + sep, + end: root ? undefined : selfKey + succ(sep), + } +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..5a8993e --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,47 @@ +/** Base class for all PotentialStorage errors. */ +export class PotentialStorageError extends Error { + constructor(message: string) { + super(message) + this.name = new.target.name + } +} + +/** An optimistic-concurrency check failed (stale `expectedVersion` or a lost commit race). */ +export class ConcurrencyError extends PotentialStorageError { + constructor( + readonly stream: string, + readonly expectedVersion: number | 'any', + readonly actualVersion: number, + ) { + super(`Concurrency conflict on "${stream}": expected version ${expectedVersion}, actual ${actualVersion}`) + } +} + +/** A unique index already holds the given value for another key. */ +export class UniqueConstraintError extends PotentialStorageError { + constructor( + readonly index: string, + readonly values: readonly unknown[], + readonly existingKey?: string, + ) { + super(`Unique index "${index}" already contains ${JSON.stringify(values)}${existingKey === undefined ? '' : ` (key "${existingKey}")`}`) + } +} + +/** The driver lacks an optional capability required by the requested feature. */ +export class CapabilityError extends PotentialStorageError { + constructor(readonly capability: string, detail?: string) { + super(`Driver lacks capability "${capability}"${detail ? `: ${detail}` : ''}`) + } +} + +/** + * A key argument is invalid: passed but `undefined`/`null`, empty, or otherwise unusable. + * Extends `TypeError` so callers can catch either. + */ +export class InvalidKeyError extends TypeError { + constructor(message: string) { + super(message) + this.name = 'InvalidKeyError' + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..8195f35 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export * from './errors.js' +export * from './core/path.js' +export * from './core/encoding.js' diff --git a/test/core/encoding.test.ts b/test/core/encoding.test.ts new file mode 100644 index 0000000..35ff6d2 --- /dev/null +++ b/test/core/encoding.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest' +import fc from 'fast-check' +import { decodeIndexValue, encodeIndexValue, type IndexValue } from '../../src/core/encoding.js' + +const separators = fc.constantFrom('/', ':', '.', '|', '#', '!') + +const indexValue: fc.Arbitrary = fc.oneof( + fc.constant(null), + fc.boolean(), + fc.double({ noNaN: true }), + fc.integer(), + fc.string({ unit: 'binary' }), + fc.string({ unit: fc.constantFrom('/', ':', '0', '1', 'a', '%', '~', '\0', '!') }), +) + +const rank = (v: IndexValue) => (v === null ? 0 : v === false ? 1 : v === true ? 2 : typeof v === 'number' ? 3 : 4) + +function compare(a: IndexValue, b: IndexValue): number { + const ra = rank(a) + const rb = rank(b) + if (ra !== rb) return ra - rb + if (typeof a === 'number' && typeof b === 'number') return a === b ? 0 : a < b ? -1 : 1 + if (typeof a === 'string' && typeof b === 'string') return a === b ? 0 : a < b ? -1 : 1 + return 0 +} + +const sign = (n: number) => (n < 0 ? -1 : n > 0 ? 1 : 0) +const cmpStr = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) + +describe('encodeIndexValue', () => { + it('round-trips (property)', () => { + fc.assert( + fc.property(indexValue, separators, (v, sep) => { + const decoded = decodeIndexValue(encodeIndexValue(v, sep), sep) + if (typeof v === 'number' && v === 0) expect(decoded).toBe(0) // -0 normalised + else expect(decoded).toEqual(v) + }), + ) + }) + + it('emits only characters above the separator (property)', () => { + fc.assert( + fc.property(indexValue, separators, (v, sep) => { + for (const c of encodeIndexValue(v, sep)) expect(c > sep).toBe(true) + }), + ) + }) + + it('preserves order, including when followed by a separator and suffix (property)', () => { + fc.assert( + fc.property(indexValue, indexValue, fc.string(), fc.string(), separators, (a, b, sa, sb, sep) => { + const ea = encodeIndexValue(a, sep) + const eb = encodeIndexValue(b, sep) + const expected = sign(compare(a, b)) + expect(cmpStr(ea, eb)).toBe(expected) + if (expected !== 0) expect(cmpStr(ea + sep + sa, eb + sep + sb)).toBe(expected) + }), + { numRuns: 2000 }, + ) + }) + + it('orders types null < false < true < numbers < strings', () => { + const values: IndexValue[] = ['', 'a', -Infinity, -1, 0, 1e300, Infinity, true, false, null] + const sorted = [...values].sort((a, b) => cmpStr(encodeIndexValue(a), encodeIndexValue(b))) + expect(sorted).toEqual([null, false, true, -Infinity, -1, 0, 1e300, Infinity, '', 'a']) + }) + + it('rejects NaN and non-index values', () => { + expect(() => encodeIndexValue(NaN)).toThrow() + expect(() => encodeIndexValue({} as never)).toThrow() + }) +}) diff --git a/test/core/path.test.ts b/test/core/path.test.ts new file mode 100644 index 0000000..f87a9fa --- /dev/null +++ b/test/core/path.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest' +import fc from 'fast-check' +import { + escapeSegment, + joinPath, + pathKeys, + splitPath, + succ, + toSegment, + unescapeSegment, + validateSeparator, +} from '../../src/core/path.js' +import { InvalidKeyError } from '../../src/errors.js' + +const separators = fc.constantFrom('/', ':', '.', '|', '#', '\\', '!') + +describe('escapeSegment', () => { + it('escapes %, the separator and a leading ~', () => { + expect(escapeSegment('a/b')).toBe('a%2Fb') + expect(escapeSegment('50%')).toBe('50%25') + expect(escapeSegment('~x')).toBe('%7Ex') + expect(escapeSegment('x~')).toBe('x~') + expect(escapeSegment('a:b', ':')).toBe('a%3Ab') + expect(escapeSegment('a/b', ':')).toBe('a/b') + expect(escapeSegment('plain')).toBe('plain') + }) + + it('round-trips (property)', () => { + fc.assert( + fc.property(fc.string({ unit: 'binary' }), separators, (s, sep) => { + expect(unescapeSegment(escapeSegment(s, sep))).toBe(s) + }), + ) + }) + + it('never emits a raw separator or a leading ~ (property)', () => { + fc.assert( + fc.property(fc.string({ unit: 'binary' }), separators, (s, sep) => { + const e = escapeSegment(s, sep) + expect(e.includes(sep)).toBe(false) + expect(e.startsWith('~')).toBe(false) + }), + ) + }) + + it('joins and splits paths (property)', () => { + fc.assert( + fc.property(fc.array(fc.string({ minLength: 1 }), { maxLength: 6 }), separators, (path, sep) => { + expect(splitPath(joinPath(path, sep), sep)).toEqual(path) + }), + ) + }) +}) + +describe('pathKeys', () => { + it('precomputes selfKey, prefix and subtree end', () => { + expect(pathKeys(['users', '123'], '/')).toMatchObject({ selfKey: 'users/123', prefix: 'users/123/', end: 'users/1230' }) + expect(pathKeys([], '/')).toMatchObject({ selfKey: '', prefix: '', end: undefined }) + expect(pathKeys(['a/b'], '/').selfKey).toBe('a%2Fb') + }) +}) + +describe('misc', () => { + it('succ returns the next char', () => { + expect(succ('/')).toBe('0') + }) + + it('toSegment rejects undefined, null, empty and non-finite', () => { + expect(() => toSegment(undefined)).toThrow(InvalidKeyError) + expect(() => toSegment(null)).toThrow(TypeError) + expect(() => toSegment('')).toThrow(InvalidKeyError) + expect(() => toSegment(NaN)).toThrow(InvalidKeyError) + expect(toSegment(12)).toBe('12') + }) + + it('validates separators', () => { + expect(() => validateSeparator('//')).toThrow() + expect(() => validateSeparator('%')).toThrow() + expect(() => validateSeparator('~')).toThrow() + expect(() => validateSeparator(':')).not.toThrow() + }) +}) diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..077691c --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist" }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a33d2d0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "exactOptionalPropertyTypes": false, + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": ".", + "skipLibCheck": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + }, + "include": ["src", "test", "bench"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a4568f3 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + typecheck: { enabled: true, include: ['test/**/*.test-d.ts'] }, + }, + bench: { include: ['bench/**/*.bench.ts'] }, +})