@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 +17 -0
- package/README.md +52 -2
- package/dist/cjs/index.d.ts +27 -0
- package/dist/cjs/index.js +171 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +166 -0
- package/package.json +61 -4
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
|
-
#
|
|
1
|
+
# @lp-labs/transactional-store
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|