@uploadista/kv-store-memory 0.0.13-beta.4 → 0.0.13
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/package.json +11 -6
- package/tests/memory-kv-store.test.ts +328 -0
- package/vitest.config.ts +39 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uploadista/kv-store-memory",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.13
|
|
4
|
+
"version": "0.0.13",
|
|
5
5
|
"description": "Memory KV store for Uploadista",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Uploadista",
|
|
@@ -14,17 +14,22 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"effect": "3.19.
|
|
18
|
-
"@uploadista/core": "0.0.13
|
|
17
|
+
"effect": "3.19.3",
|
|
18
|
+
"@uploadista/core": "0.0.13"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"@effect/vitest": "0.27.0",
|
|
22
|
+
"tsdown": "0.16.3",
|
|
23
|
+
"vitest": "4.0.8",
|
|
24
|
+
"@uploadista/typescript-config": "0.0.13"
|
|
23
25
|
},
|
|
24
26
|
"scripts": {
|
|
25
27
|
"build": "tsdown",
|
|
28
|
+
"check": "biome check --write ./src",
|
|
26
29
|
"format": "biome format --write ./src",
|
|
27
30
|
"lint": "biome lint --write ./src",
|
|
28
|
-
"
|
|
31
|
+
"test": "vitest",
|
|
32
|
+
"test:run": "vitest run",
|
|
33
|
+
"test:watch": "vitest --watch"
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { makeMemoryBaseKvStore } from "../src/memory-kv-store";
|
|
4
|
+
|
|
5
|
+
describe("Memory KV Store", () => {
|
|
6
|
+
describe("Basic Operations", () => {
|
|
7
|
+
it("should store and retrieve values", async () => {
|
|
8
|
+
const store = makeMemoryBaseKvStore();
|
|
9
|
+
|
|
10
|
+
await Effect.runPromise(
|
|
11
|
+
Effect.gen(function* () {
|
|
12
|
+
yield* store.set("key1", "value1");
|
|
13
|
+
const value = yield* store.get("key1");
|
|
14
|
+
expect(value).toBe("value1");
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return null for non-existent keys", async () => {
|
|
20
|
+
const store = makeMemoryBaseKvStore();
|
|
21
|
+
|
|
22
|
+
await Effect.runPromise(
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
const value = yield* store.get("non-existent");
|
|
25
|
+
expect(value).toBeNull();
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should delete values", async () => {
|
|
31
|
+
const store = makeMemoryBaseKvStore();
|
|
32
|
+
|
|
33
|
+
await Effect.runPromise(
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
yield* store.set("key1", "value1");
|
|
36
|
+
yield* store.delete("key1");
|
|
37
|
+
const value = yield* store.get("key1");
|
|
38
|
+
expect(value).toBeNull();
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("Complex Data Types", () => {
|
|
45
|
+
it("should store and retrieve JSON strings", async () => {
|
|
46
|
+
const store = makeMemoryBaseKvStore();
|
|
47
|
+
|
|
48
|
+
await Effect.runPromise(
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const obj = { name: "John", age: 30 };
|
|
51
|
+
yield* store.set("user1", JSON.stringify(obj));
|
|
52
|
+
const retrieved = yield* store.get("user1");
|
|
53
|
+
expect(retrieved).not.toBeNull();
|
|
54
|
+
expect(JSON.parse(retrieved as string)).toEqual(obj);
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should store and retrieve JSON arrays", async () => {
|
|
60
|
+
const store = makeMemoryBaseKvStore();
|
|
61
|
+
|
|
62
|
+
await Effect.runPromise(
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const arr = [1, 2, 3, 4, 5];
|
|
65
|
+
yield* store.set("numbers", JSON.stringify(arr));
|
|
66
|
+
const retrieved = yield* store.get("numbers");
|
|
67
|
+
expect(retrieved).not.toBeNull();
|
|
68
|
+
expect(JSON.parse(retrieved as string)).toEqual(arr);
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should handle empty strings", async () => {
|
|
74
|
+
const store = makeMemoryBaseKvStore();
|
|
75
|
+
|
|
76
|
+
await Effect.runPromise(
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
yield* store.set("key1", "");
|
|
79
|
+
const value = yield* store.get("key1");
|
|
80
|
+
expect(value).toBe("");
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should handle unicode characters", async () => {
|
|
86
|
+
const store = makeMemoryBaseKvStore();
|
|
87
|
+
const unicodeValue = "Hello 世界 🌍";
|
|
88
|
+
|
|
89
|
+
await Effect.runPromise(
|
|
90
|
+
Effect.gen(function* () {
|
|
91
|
+
yield* store.set("unicode", unicodeValue);
|
|
92
|
+
const value = yield* store.get("unicode");
|
|
93
|
+
expect(value).toBe(unicodeValue);
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should handle multiline strings", async () => {
|
|
99
|
+
const store = makeMemoryBaseKvStore();
|
|
100
|
+
const multilineValue = "line1\nline2\nline3";
|
|
101
|
+
|
|
102
|
+
await Effect.runPromise(
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
yield* store.set("multiline", multilineValue);
|
|
105
|
+
const value = yield* store.get("multiline");
|
|
106
|
+
expect(value).toBe(multilineValue);
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("List Operations", () => {
|
|
113
|
+
it("should list all keys", async () => {
|
|
114
|
+
const store = makeMemoryBaseKvStore();
|
|
115
|
+
|
|
116
|
+
await Effect.runPromise(
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
yield* store.set("key1", "value1");
|
|
119
|
+
yield* store.set("key2", "value2");
|
|
120
|
+
yield* store.set("key3", "value3");
|
|
121
|
+
|
|
122
|
+
if (!store.list) throw new Error("list not supported");
|
|
123
|
+
const keys = yield* store.list("");
|
|
124
|
+
expect(keys).toHaveLength(3);
|
|
125
|
+
expect(keys).toContain("key1");
|
|
126
|
+
expect(keys).toContain("key2");
|
|
127
|
+
expect(keys).toContain("key3");
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should list keys with prefix", async () => {
|
|
133
|
+
const store = makeMemoryBaseKvStore();
|
|
134
|
+
|
|
135
|
+
await Effect.runPromise(
|
|
136
|
+
Effect.gen(function* () {
|
|
137
|
+
yield* store.set("user:1", "John");
|
|
138
|
+
yield* store.set("user:2", "Jane");
|
|
139
|
+
yield* store.set("post:1", "Post content");
|
|
140
|
+
|
|
141
|
+
if (!store.list) throw new Error("list not supported");
|
|
142
|
+
const userKeys = yield* store.list("user:");
|
|
143
|
+
expect(userKeys).toHaveLength(2);
|
|
144
|
+
expect(userKeys).toContain("user:1");
|
|
145
|
+
expect(userKeys).toContain("user:2");
|
|
146
|
+
expect(userKeys).not.toContain("post:1");
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should return empty list for non-matching prefix", async () => {
|
|
152
|
+
const store = makeMemoryBaseKvStore();
|
|
153
|
+
|
|
154
|
+
await Effect.runPromise(
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
yield* store.set("key1", "value1");
|
|
157
|
+
if (!store.list) throw new Error("list not supported");
|
|
158
|
+
const keys = yield* store.list("nonexistent:");
|
|
159
|
+
expect(keys).toHaveLength(0);
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("Update Operations", () => {
|
|
166
|
+
it("should update existing values", async () => {
|
|
167
|
+
const store = makeMemoryBaseKvStore();
|
|
168
|
+
|
|
169
|
+
await Effect.runPromise(
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
yield* store.set("key1", "value1");
|
|
172
|
+
yield* store.set("key1", "value2");
|
|
173
|
+
const value = yield* store.get("key1");
|
|
174
|
+
expect(value).toBe("value2");
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should handle concurrent updates", async () => {
|
|
180
|
+
const store = makeMemoryBaseKvStore();
|
|
181
|
+
|
|
182
|
+
await Effect.runPromise(
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
yield* store.set("counter", "0");
|
|
185
|
+
|
|
186
|
+
// Concurrent updates
|
|
187
|
+
yield* Effect.all(
|
|
188
|
+
[
|
|
189
|
+
store.set("counter", "1"),
|
|
190
|
+
store.set("counter", "2"),
|
|
191
|
+
store.set("counter", "3"),
|
|
192
|
+
],
|
|
193
|
+
{ concurrency: "unbounded" },
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const value = yield* store.get("counter");
|
|
197
|
+
expect(["1", "2", "3"]).toContain(value); // One of the values should win
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should preserve other keys when updating", async () => {
|
|
203
|
+
const store = makeMemoryBaseKvStore();
|
|
204
|
+
|
|
205
|
+
await Effect.runPromise(
|
|
206
|
+
Effect.gen(function* () {
|
|
207
|
+
yield* store.set("key1", "value1");
|
|
208
|
+
yield* store.set("key2", "value2");
|
|
209
|
+
yield* store.set("key1", "updated");
|
|
210
|
+
|
|
211
|
+
const value1 = yield* store.get("key1");
|
|
212
|
+
const value2 = yield* store.get("key2");
|
|
213
|
+
|
|
214
|
+
expect(value1).toBe("updated");
|
|
215
|
+
expect(value2).toBe("value2");
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("Isolation", () => {
|
|
222
|
+
it("should maintain separate stores", async () => {
|
|
223
|
+
const store1 = makeMemoryBaseKvStore();
|
|
224
|
+
const store2 = makeMemoryBaseKvStore();
|
|
225
|
+
|
|
226
|
+
await Effect.runPromise(
|
|
227
|
+
Effect.gen(function* () {
|
|
228
|
+
yield* store1.set("key1", "store1-value");
|
|
229
|
+
yield* store2.set("key1", "store2-value");
|
|
230
|
+
|
|
231
|
+
const value1 = yield* store1.get("key1");
|
|
232
|
+
const value2 = yield* store2.get("key1");
|
|
233
|
+
|
|
234
|
+
expect(value1).toBe("store1-value");
|
|
235
|
+
expect(value2).toBe("store2-value");
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("Performance", () => {
|
|
242
|
+
it("should handle large numbers of keys", async () => {
|
|
243
|
+
const store = makeMemoryBaseKvStore();
|
|
244
|
+
const keyCount = 1000;
|
|
245
|
+
|
|
246
|
+
await Effect.runPromise(
|
|
247
|
+
Effect.gen(function* () {
|
|
248
|
+
// Set many keys
|
|
249
|
+
for (let i = 0; i < keyCount; i++) {
|
|
250
|
+
yield* store.set(`key${i}`, `${i}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Verify count
|
|
254
|
+
if (!store.list) throw new Error("list not supported");
|
|
255
|
+
const keys = yield* store.list("");
|
|
256
|
+
expect(keys).toHaveLength(keyCount);
|
|
257
|
+
|
|
258
|
+
// Verify random access
|
|
259
|
+
const value500 = yield* store.get("key500");
|
|
260
|
+
expect(value500).toBe("500");
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
}, 10000);
|
|
264
|
+
|
|
265
|
+
it("should handle large values", async () => {
|
|
266
|
+
const store = makeMemoryBaseKvStore();
|
|
267
|
+
const largeValue = "x".repeat(100000);
|
|
268
|
+
|
|
269
|
+
await Effect.runPromise(
|
|
270
|
+
Effect.gen(function* () {
|
|
271
|
+
yield* store.set("large", largeValue);
|
|
272
|
+
const value = yield* store.get("large");
|
|
273
|
+
expect(value).not.toBeNull();
|
|
274
|
+
expect(value?.length).toBe(100000);
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("should handle keys with special characters", async () => {
|
|
280
|
+
const store = makeMemoryBaseKvStore();
|
|
281
|
+
|
|
282
|
+
await Effect.runPromise(
|
|
283
|
+
Effect.gen(function* () {
|
|
284
|
+
const specialKey = "user_123_profile@email.com";
|
|
285
|
+
yield* store.set(specialKey, "value");
|
|
286
|
+
const value = yield* store.get(specialKey);
|
|
287
|
+
expect(value).toBe("value");
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("Deletion", () => {
|
|
294
|
+
it("should handle deletion of non-existent key", async () => {
|
|
295
|
+
const store = makeMemoryBaseKvStore();
|
|
296
|
+
|
|
297
|
+
await Effect.runPromise(
|
|
298
|
+
Effect.gen(function* () {
|
|
299
|
+
yield* store.delete("nonexistent");
|
|
300
|
+
const value = yield* store.get("nonexistent");
|
|
301
|
+
expect(value).toBeNull();
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should remove key from list after deletion", async () => {
|
|
307
|
+
const store = makeMemoryBaseKvStore();
|
|
308
|
+
|
|
309
|
+
await Effect.runPromise(
|
|
310
|
+
Effect.gen(function* () {
|
|
311
|
+
yield* store.set("key1", "value1");
|
|
312
|
+
yield* store.set("key2", "value2");
|
|
313
|
+
|
|
314
|
+
if (!store.list) throw new Error("list not supported");
|
|
315
|
+
const keysBefore = yield* store.list("");
|
|
316
|
+
expect(keysBefore).toHaveLength(2);
|
|
317
|
+
|
|
318
|
+
yield* store.delete("key1");
|
|
319
|
+
|
|
320
|
+
const keysAfter = yield* store.list("");
|
|
321
|
+
expect(keysAfter).toHaveLength(1);
|
|
322
|
+
expect(keysAfter).not.toContain("key1");
|
|
323
|
+
expect(keysAfter).toContain("key2");
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared vitest configuration template for uploadista-sdk packages
|
|
5
|
+
*
|
|
6
|
+
* This template should be used by all SDK packages to ensure consistent
|
|
7
|
+
* testing configuration across the monorepo.
|
|
8
|
+
*
|
|
9
|
+
* Key features:
|
|
10
|
+
* - Tests in dedicated `tests/` directories (not colocated with src)
|
|
11
|
+
* - Node environment for server-side code
|
|
12
|
+
* - V8 coverage provider
|
|
13
|
+
* - Global test functions available
|
|
14
|
+
* - Effect testing support via @effect/vitest
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* Copy this file to your package root as `vitest.config.ts` and customize
|
|
18
|
+
* if needed (though most packages should use this as-is).
|
|
19
|
+
*/
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
test: {
|
|
22
|
+
globals: true,
|
|
23
|
+
environment: "node",
|
|
24
|
+
include: ["tests/**/*.test.ts"],
|
|
25
|
+
exclude: ["node_modules", "dist"],
|
|
26
|
+
coverage: {
|
|
27
|
+
provider: "v8",
|
|
28
|
+
reporter: ["text", "json", "html"],
|
|
29
|
+
exclude: [
|
|
30
|
+
"node_modules/",
|
|
31
|
+
"dist/",
|
|
32
|
+
"**/*.d.ts",
|
|
33
|
+
"**/*.test.ts",
|
|
34
|
+
"**/*.spec.ts",
|
|
35
|
+
"tests/",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|