@lp-labs/transactional-store 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Libin Prasanth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # @lp-labs/transactional-store
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A compact, bounded in-memory JSON key-value store with atomic staged transactions.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @lp-labs/transactional-store
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```js
14
+ import { createTransactionalStore } from '@lp-labs/transactional-store';
15
+
16
+ const store = createTransactionalStore({ maxEntries: 100 });
17
+ const tx = store.begin();
18
+ tx.set('profile:7', { name: 'Ada' });
19
+ tx.commit();
20
+ console.log(store.get('profile:7'));
21
+ ```
22
+
23
+ ## API
24
+
25
+ `createTransactionalStore({ maxEntries?, maxWrites? })` creates a JSON-only store.
26
+ `begin()` returns a transaction with `get`, `set`, `delete`, `commit`, and
27
+ `rollback`. Reads within a transaction see its staged changes and current store
28
+ values. `commit()` applies all staged writes synchronously or changes nothing;
29
+ it returns write/delete counts. Capacity errors use `StoreError`; a failed
30
+ capacity commit stays open so callers can roll it back. `snapshot()` returns a
31
+ detached record. Default limits are 10,000 entries and 1,000 distinct writes per
32
+ transaction; JSON values are limited to 10,000 nodes, 32 nesting levels, and
33
+ 1,000,000 characters.
34
+
35
+ ## Customization
36
+
37
+ Set `maxEntries` and `maxWrites` when creating a store. Store only plain JSON
38
+ values; returned values are detached copies and do not mutate stored state.
39
+
40
+ ## Limitations
41
+
42
+ State exists only in this process and is lost when the process exits. This is
43
+ not durable storage, a concurrent transaction scheduler, or a database with
44
+ isolation guarantees. Transactions commit synchronously; concurrent external
45
+ systems are not coordinated.
46
+
47
+ ## Support
48
+
49
+ If this library is useful to you, consider supporting its maintenance with a [donation via PayPal](https://www.paypal.me/LibinPrasanth).
50
+
51
+ ## Author
52
+
53
+ Created by [Libin Prasanth](https://www.linkedin.com/in/libinprasanth/). Connect on LinkedIn for updates and discussion.
@@ -0,0 +1,27 @@
1
+ export type Json = null | boolean | number | string | Json[] | {
2
+ [key: string]: Json;
3
+ };
4
+ export interface StoreOptions {
5
+ maxEntries?: number;
6
+ maxWrites?: number;
7
+ }
8
+ export interface Transaction {
9
+ get(key: string): Json | undefined;
10
+ set(key: string, value: Json): void;
11
+ delete(key: string): void;
12
+ commit(): {
13
+ written: number;
14
+ deleted: number;
15
+ };
16
+ rollback(): boolean;
17
+ }
18
+ export interface TransactionalStore {
19
+ begin(): Transaction;
20
+ get(key: string): Json | undefined;
21
+ snapshot(): Record<string, Json>;
22
+ }
23
+ export declare class StoreError extends Error {
24
+ readonly code: 'invalid' | 'capacity' | 'closed';
25
+ constructor(code: 'invalid' | 'capacity' | 'closed', message: string);
26
+ }
27
+ export declare function createTransactionalStore(options?: StoreOptions): TransactionalStore;
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StoreError = void 0;
4
+ exports.createTransactionalStore = createTransactionalStore;
5
+ class StoreError extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = 'StoreError';
11
+ }
12
+ }
13
+ exports.StoreError = StoreError;
14
+ const deleted = Symbol('deleted');
15
+ function keyOf(key) {
16
+ if (typeof key !== 'string' || key.length < 1 || key.length > 256)
17
+ throw new StoreError('invalid', 'Keys must contain 1..256 characters.');
18
+ }
19
+ function clone(value) {
20
+ let nodes = 0;
21
+ let characters = 0;
22
+ const visit = (item, depth) => {
23
+ if (++nodes > 10_000 || depth > 32)
24
+ throw new StoreError('capacity', 'Value exceeds JSON limits.');
25
+ if (item === null || typeof item === 'boolean')
26
+ return item;
27
+ if (typeof item === 'string') {
28
+ characters += item.length;
29
+ if (characters > 1_000_000)
30
+ throw new StoreError('capacity', 'Value exceeds the text limit.');
31
+ return item;
32
+ }
33
+ if (typeof item === 'number' && Number.isFinite(item))
34
+ return item;
35
+ if (Array.isArray(item)) {
36
+ if (Reflect.ownKeys(item).length !== item.length + 1)
37
+ throw new StoreError('invalid', 'Arrays must be dense and contain no extra properties.');
38
+ const result = [];
39
+ for (let index = 0; index < item.length; index++) {
40
+ const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
41
+ if (!descriptor?.enumerable || !('value' in descriptor))
42
+ throw new StoreError('invalid', 'Arrays must contain enumerable data elements.');
43
+ result.push(visit(descriptor.value, depth + 1));
44
+ }
45
+ return result;
46
+ }
47
+ if (typeof item !== 'object' ||
48
+ (Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null))
49
+ throw new StoreError('invalid', 'Values must be plain JSON-compatible data.');
50
+ const entries = [];
51
+ for (const name of Reflect.ownKeys(item)) {
52
+ const descriptor = typeof name === 'string' ? Object.getOwnPropertyDescriptor(item, name) : undefined;
53
+ if (typeof name !== 'string' || !descriptor?.enumerable || !('value' in descriptor))
54
+ throw new StoreError('invalid', 'Objects must use enumerable string data properties.');
55
+ if (name.length > 256)
56
+ throw new StoreError('invalid', 'Object keys are limited to 256 characters.');
57
+ characters += name.length;
58
+ if (characters > 1_000_000)
59
+ throw new StoreError('capacity', 'Value exceeds the text limit.');
60
+ entries.push([name, visit(descriptor.value, depth + 1)]);
61
+ }
62
+ return Object.fromEntries(entries);
63
+ };
64
+ return visit(value, 0);
65
+ }
66
+ function limit(value, name, max = 1_000_000) {
67
+ if (!Number.isSafeInteger(value) || value < 1 || value > max)
68
+ throw new StoreError('invalid', `${name} must be an integer from 1 to ${max}.`);
69
+ return value;
70
+ }
71
+ function createTransactionalStore(options = {}) {
72
+ if (options === null ||
73
+ typeof options !== 'object' ||
74
+ Array.isArray(options) ||
75
+ (Object.getPrototypeOf(options) !== Object.prototype && Object.getPrototypeOf(options) !== null))
76
+ throw new StoreError('invalid', 'Options must be a plain object.');
77
+ for (const key of Reflect.ownKeys(options)) {
78
+ const descriptor = typeof key === 'string' ? Object.getOwnPropertyDescriptor(options, key) : undefined;
79
+ if (typeof key !== 'string' ||
80
+ !['maxEntries', 'maxWrites'].includes(key) ||
81
+ !descriptor?.enumerable ||
82
+ !('value' in descriptor) ||
83
+ typeof descriptor.value !== 'number')
84
+ throw new StoreError('invalid', 'Options contain an unsupported or invalid field.');
85
+ }
86
+ const maxEntries = limit(options.maxEntries ?? 10_000, 'maxEntries');
87
+ const maxWrites = limit(options.maxWrites ?? 1000, 'maxWrites', 100_000);
88
+ const values = new Map();
89
+ const ensurePresent = (key) => {
90
+ keyOf(key);
91
+ return values.get(key);
92
+ };
93
+ return {
94
+ get(key) {
95
+ const value = ensurePresent(key);
96
+ return value === undefined ? undefined : clone(value);
97
+ },
98
+ snapshot() {
99
+ return Object.fromEntries([...values].map(([key, value]) => [key, clone(value)]));
100
+ },
101
+ begin() {
102
+ const changes = new Map();
103
+ let open = true;
104
+ const assertOpen = () => {
105
+ if (!open)
106
+ throw new StoreError('closed', 'Transaction is no longer open.');
107
+ };
108
+ return {
109
+ get(key) {
110
+ assertOpen();
111
+ keyOf(key);
112
+ const staged = changes.get(key);
113
+ if (staged === deleted)
114
+ return undefined;
115
+ if (staged !== undefined)
116
+ return clone(staged);
117
+ const value = values.get(key);
118
+ return value === undefined ? undefined : clone(value);
119
+ },
120
+ set(key, value) {
121
+ assertOpen();
122
+ keyOf(key);
123
+ if (!changes.has(key) && changes.size >= maxWrites)
124
+ throw new StoreError('capacity', 'Transaction write limit reached.');
125
+ changes.set(key, clone(value));
126
+ },
127
+ delete(key) {
128
+ assertOpen();
129
+ keyOf(key);
130
+ if (!changes.has(key) && changes.size >= maxWrites)
131
+ throw new StoreError('capacity', 'Transaction write limit reached.');
132
+ changes.set(key, deleted);
133
+ },
134
+ commit() {
135
+ assertOpen();
136
+ let size = values.size;
137
+ for (const [key, value] of changes) {
138
+ const existed = values.has(key);
139
+ if (value === deleted && existed)
140
+ size--;
141
+ else if (value !== deleted && !existed)
142
+ size++;
143
+ }
144
+ if (size > maxEntries)
145
+ throw new StoreError('capacity', 'Commit would exceed maxEntries.');
146
+ let written = 0;
147
+ let removed = 0;
148
+ for (const [key, value] of changes) {
149
+ if (value === deleted) {
150
+ if (values.delete(key))
151
+ removed++;
152
+ }
153
+ else {
154
+ values.set(key, clone(value));
155
+ written++;
156
+ }
157
+ }
158
+ open = false;
159
+ return { written, deleted: removed };
160
+ },
161
+ rollback() {
162
+ if (!open)
163
+ return false;
164
+ open = false;
165
+ changes.clear();
166
+ return true;
167
+ },
168
+ };
169
+ },
170
+ };
171
+ }
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,27 @@
1
+ export type Json = null | boolean | number | string | Json[] | {
2
+ [key: string]: Json;
3
+ };
4
+ export interface StoreOptions {
5
+ maxEntries?: number;
6
+ maxWrites?: number;
7
+ }
8
+ export interface Transaction {
9
+ get(key: string): Json | undefined;
10
+ set(key: string, value: Json): void;
11
+ delete(key: string): void;
12
+ commit(): {
13
+ written: number;
14
+ deleted: number;
15
+ };
16
+ rollback(): boolean;
17
+ }
18
+ export interface TransactionalStore {
19
+ begin(): Transaction;
20
+ get(key: string): Json | undefined;
21
+ snapshot(): Record<string, Json>;
22
+ }
23
+ export declare class StoreError extends Error {
24
+ readonly code: 'invalid' | 'capacity' | 'closed';
25
+ constructor(code: 'invalid' | 'capacity' | 'closed', message: string);
26
+ }
27
+ export declare function createTransactionalStore(options?: StoreOptions): TransactionalStore;
@@ -0,0 +1,166 @@
1
+ export class StoreError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'StoreError';
7
+ }
8
+ }
9
+ const deleted = Symbol('deleted');
10
+ function keyOf(key) {
11
+ if (typeof key !== 'string' || key.length < 1 || key.length > 256)
12
+ throw new StoreError('invalid', 'Keys must contain 1..256 characters.');
13
+ }
14
+ function clone(value) {
15
+ let nodes = 0;
16
+ let characters = 0;
17
+ const visit = (item, depth) => {
18
+ if (++nodes > 10_000 || depth > 32)
19
+ throw new StoreError('capacity', 'Value exceeds JSON limits.');
20
+ if (item === null || typeof item === 'boolean')
21
+ return item;
22
+ if (typeof item === 'string') {
23
+ characters += item.length;
24
+ if (characters > 1_000_000)
25
+ throw new StoreError('capacity', 'Value exceeds the text limit.');
26
+ return item;
27
+ }
28
+ if (typeof item === 'number' && Number.isFinite(item))
29
+ return item;
30
+ if (Array.isArray(item)) {
31
+ if (Reflect.ownKeys(item).length !== item.length + 1)
32
+ throw new StoreError('invalid', 'Arrays must be dense and contain no extra properties.');
33
+ const result = [];
34
+ for (let index = 0; index < item.length; index++) {
35
+ const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
36
+ if (!descriptor?.enumerable || !('value' in descriptor))
37
+ throw new StoreError('invalid', 'Arrays must contain enumerable data elements.');
38
+ result.push(visit(descriptor.value, depth + 1));
39
+ }
40
+ return result;
41
+ }
42
+ if (typeof item !== 'object' ||
43
+ (Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null))
44
+ throw new StoreError('invalid', 'Values must be plain JSON-compatible data.');
45
+ const entries = [];
46
+ for (const name of Reflect.ownKeys(item)) {
47
+ const descriptor = typeof name === 'string' ? Object.getOwnPropertyDescriptor(item, name) : undefined;
48
+ if (typeof name !== 'string' || !descriptor?.enumerable || !('value' in descriptor))
49
+ throw new StoreError('invalid', 'Objects must use enumerable string data properties.');
50
+ if (name.length > 256)
51
+ throw new StoreError('invalid', 'Object keys are limited to 256 characters.');
52
+ characters += name.length;
53
+ if (characters > 1_000_000)
54
+ throw new StoreError('capacity', 'Value exceeds the text limit.');
55
+ entries.push([name, visit(descriptor.value, depth + 1)]);
56
+ }
57
+ return Object.fromEntries(entries);
58
+ };
59
+ return visit(value, 0);
60
+ }
61
+ function limit(value, name, max = 1_000_000) {
62
+ if (!Number.isSafeInteger(value) || value < 1 || value > max)
63
+ throw new StoreError('invalid', `${name} must be an integer from 1 to ${max}.`);
64
+ return value;
65
+ }
66
+ export function createTransactionalStore(options = {}) {
67
+ if (options === null ||
68
+ typeof options !== 'object' ||
69
+ Array.isArray(options) ||
70
+ (Object.getPrototypeOf(options) !== Object.prototype && Object.getPrototypeOf(options) !== null))
71
+ throw new StoreError('invalid', 'Options must be a plain object.');
72
+ for (const key of Reflect.ownKeys(options)) {
73
+ const descriptor = typeof key === 'string' ? Object.getOwnPropertyDescriptor(options, key) : undefined;
74
+ if (typeof key !== 'string' ||
75
+ !['maxEntries', 'maxWrites'].includes(key) ||
76
+ !descriptor?.enumerable ||
77
+ !('value' in descriptor) ||
78
+ typeof descriptor.value !== 'number')
79
+ throw new StoreError('invalid', 'Options contain an unsupported or invalid field.');
80
+ }
81
+ const maxEntries = limit(options.maxEntries ?? 10_000, 'maxEntries');
82
+ const maxWrites = limit(options.maxWrites ?? 1000, 'maxWrites', 100_000);
83
+ const values = new Map();
84
+ const ensurePresent = (key) => {
85
+ keyOf(key);
86
+ return values.get(key);
87
+ };
88
+ return {
89
+ get(key) {
90
+ const value = ensurePresent(key);
91
+ return value === undefined ? undefined : clone(value);
92
+ },
93
+ snapshot() {
94
+ return Object.fromEntries([...values].map(([key, value]) => [key, clone(value)]));
95
+ },
96
+ begin() {
97
+ const changes = new Map();
98
+ let open = true;
99
+ const assertOpen = () => {
100
+ if (!open)
101
+ throw new StoreError('closed', 'Transaction is no longer open.');
102
+ };
103
+ return {
104
+ get(key) {
105
+ assertOpen();
106
+ keyOf(key);
107
+ const staged = changes.get(key);
108
+ if (staged === deleted)
109
+ return undefined;
110
+ if (staged !== undefined)
111
+ return clone(staged);
112
+ const value = values.get(key);
113
+ return value === undefined ? undefined : clone(value);
114
+ },
115
+ set(key, value) {
116
+ assertOpen();
117
+ keyOf(key);
118
+ if (!changes.has(key) && changes.size >= maxWrites)
119
+ throw new StoreError('capacity', 'Transaction write limit reached.');
120
+ changes.set(key, clone(value));
121
+ },
122
+ delete(key) {
123
+ assertOpen();
124
+ keyOf(key);
125
+ if (!changes.has(key) && changes.size >= maxWrites)
126
+ throw new StoreError('capacity', 'Transaction write limit reached.');
127
+ changes.set(key, deleted);
128
+ },
129
+ commit() {
130
+ assertOpen();
131
+ let size = values.size;
132
+ for (const [key, value] of changes) {
133
+ const existed = values.has(key);
134
+ if (value === deleted && existed)
135
+ size--;
136
+ else if (value !== deleted && !existed)
137
+ size++;
138
+ }
139
+ if (size > maxEntries)
140
+ throw new StoreError('capacity', 'Commit would exceed maxEntries.');
141
+ let written = 0;
142
+ let removed = 0;
143
+ for (const [key, value] of changes) {
144
+ if (value === deleted) {
145
+ if (values.delete(key))
146
+ removed++;
147
+ }
148
+ else {
149
+ values.set(key, clone(value));
150
+ written++;
151
+ }
152
+ }
153
+ open = false;
154
+ return { written, deleted: removed };
155
+ },
156
+ rollback() {
157
+ if (!open)
158
+ return false;
159
+ open = false;
160
+ changes.clear();
161
+ return true;
162
+ },
163
+ };
164
+ },
165
+ };
166
+ }
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@lp-labs/transactional-store",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A bounded in-memory key-value store with atomic staged transactions.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/cjs/index.js",
8
+ "module": "./dist/esm/index.js",
9
+ "types": "./dist/esm/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./dist/esm/index.d.ts",
14
+ "default": "./dist/esm/index.js"
15
+ },
16
+ "require": {
17
+ "types": "./dist/cjs/index.d.ts",
18
+ "default": "./dist/cjs/index.js"
19
+ }
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "sideEffects": false,
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "scripts": {
33
+ "build": "node scripts/build.mjs",
34
+ "typecheck": "npm run build && node scripts/check-types.mjs",
35
+ "test": "npm run build && node --test tests/*.test.mjs",
36
+ "format": "prettier --write src scripts tests *.json README.md",
37
+ "format:check": "prettier --check src scripts tests *.json README.md",
38
+ "prepack": "npm run build"
39
+ },
40
+ "devDependencies": {
41
+ "typescript": "^5.9.3",
42
+ "prettier": "^3.6.2"
43
+ },
44
+ "prettier": {
45
+ "singleQuote": true,
46
+ "semi": true,
47
+ "trailingComma": "all",
48
+ "printWidth": 100,
49
+ "endOfLine": "lf"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "registry": "https://registry.npmjs.org/"
54
+ },
55
+ "author": {
56
+ "name": "Libin Prasanth",
57
+ "url": "https://www.linkedin.com/in/libinprasanth/"
58
+ },
59
+ "funding": {
60
+ "type": "paypal",
61
+ "url": "https://www.paypal.me/LibinPrasanth"
62
+ }
63
+ }