bff-store 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/.claude/settings.local.json +45 -0
- package/CONTEXT.md +53 -0
- package/README.md +223 -0
- package/dist/cli.js +32577 -0
- package/dist/index.d.mts +232 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.mjs +430 -0
- package/dist/package.json +62 -0
- package/dist/server/entry.d.mts +94 -0
- package/dist/server/entry.d.ts +94 -0
- package/dist/server/entry.js +573 -0
- package/dist/server/entry.mjs +533 -0
- package/dist/server-V7WCW4ZB.mjs +530 -0
- package/dist/storage/jsonl-entry.d.mts +42 -0
- package/dist/storage/jsonl-entry.d.ts +42 -0
- package/dist/storage/jsonl-entry.js +112 -0
- package/dist/storage/jsonl-entry.mjs +74 -0
- package/dist/storage/mongodb-entry.d.mts +40 -0
- package/dist/storage/mongodb-entry.d.ts +40 -0
- package/dist/storage/mongodb-entry.js +114 -0
- package/dist/storage/mongodb-entry.mjs +86 -0
- package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +104 -0
- package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +63 -0
- package/docs/BUG_FIX_SESSION_2026-06-03.md +171 -0
- package/docs/IMPLEMENTATION.md +333 -0
- package/docs/PLAN.md +153 -0
- package/docs/REMOTE_STORAGE_CONFIG.md +125 -0
- package/docs/SIDECAR_SERVER.md +184 -0
- package/package.json +62 -0
- package/scripts/adapt-dist-package.js +33 -0
- package/src/atomCreator.ts +76 -0
- package/src/createStore.ts +77 -0
- package/src/debouncer.ts +84 -0
- package/src/index.ts +35 -0
- package/src/server/cli.ts +62 -0
- package/src/server/entityIdCache.ts +57 -0
- package/src/server/entry.ts +12 -0
- package/src/server/handlers.ts +271 -0
- package/src/server/index.ts +182 -0
- package/src/server/router.ts +74 -0
- package/src/server.ts +5 -0
- package/src/storage/adapters/remoteStorage.ts +70 -0
- package/src/storage/base.ts +28 -0
- package/src/storage/index.ts +9 -0
- package/src/storage/jsonl-entry.ts +9 -0
- package/src/storage/jsonl.ts +111 -0
- package/src/storage/memory.ts +49 -0
- package/src/storage/mongodb-entry.ts +9 -0
- package/src/storage/mongodb.ts +132 -0
- package/src/storage/protocol.ts +170 -0
- package/src/storage/transport.ts +95 -0
- package/src/types.ts +76 -0
- package/src/useStore.ts +83 -0
- package/tests/atomCreator.test.ts +153 -0
- package/tests/createStore.test.ts +126 -0
- package/tests/debouncer.test.ts +125 -0
- package/tests/server.test.ts +158 -0
- package/tests/storage/jsonl.test.ts +132 -0
- package/tests/storage/memory.test.ts +101 -0
- package/tests/storage/mongodb.test.ts +40 -0
- package/tests/storage/remoteStorage.test.ts +126 -0
- package/tests/useStore.test.tsx +147 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +53 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { startServer } from '../src/server';
|
|
6
|
+
|
|
7
|
+
describe('server', () => {
|
|
8
|
+
let server: http.Server;
|
|
9
|
+
const testDir = path.join(__dirname, '../.test-server-data');
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
if (fs.existsSync(testDir)) {
|
|
13
|
+
fs.rmSync(testDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
if (server) {
|
|
20
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('health check', () => {
|
|
25
|
+
it('should return ok', async () => {
|
|
26
|
+
server = await startServer({
|
|
27
|
+
backend: 'jsonl',
|
|
28
|
+
jsonlDir: testDir,
|
|
29
|
+
port: 3849,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const response = await fetch('http://localhost:3849/health');
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
|
|
35
|
+
expect(response.status).toBe(200);
|
|
36
|
+
expect(data.status).toBe('ok');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('storage operations', () => {
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
server = await startServer({
|
|
43
|
+
backend: 'jsonl',
|
|
44
|
+
jsonlDir: testDir,
|
|
45
|
+
port: 3849,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should set and get a value', async () => {
|
|
50
|
+
await fetch('http://localhost:3849/storage/set/theme', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ value: 'dark' }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const response = await fetch('http://localhost:3849/storage/get/theme');
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
|
|
59
|
+
expect(data.value).toBe('dark');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should delete a value', async () => {
|
|
63
|
+
await fetch('http://localhost:3849/storage/set/theme', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'Content-Type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ value: 'dark' }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await fetch('http://localhost:3849/storage/delete/theme', {
|
|
70
|
+
method: 'DELETE',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const response = await fetch('http://localhost:3849/storage/get/theme');
|
|
74
|
+
const data = await response.json();
|
|
75
|
+
|
|
76
|
+
expect(data.value).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return 404 for unknown paths', async () => {
|
|
80
|
+
const response = await fetch('http://localhost:3849/unknown');
|
|
81
|
+
expect(response.status).toBe(404);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle batch-get', async () => {
|
|
85
|
+
await fetch('http://localhost:3849/storage/set/key1', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ value: 'value1' }),
|
|
89
|
+
});
|
|
90
|
+
await fetch('http://localhost:3849/storage/set/key2', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ value: 'value2' }),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const response = await fetch('http://localhost:3849/storage/batch-get', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({ keys: ['key1', 'key2'] }),
|
|
100
|
+
});
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
|
|
103
|
+
expect(response.status).toBe(200);
|
|
104
|
+
expect(data.entries.key1).toBe('value1');
|
|
105
|
+
expect(data.entries.key2).toBe('value2');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle batch-set', async () => {
|
|
109
|
+
const response = await fetch('http://localhost:3849/storage/batch-set', {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
body: JSON.stringify({ entries: { key1: 'value1', key2: 'value2' } }),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(response.status).toBe(200);
|
|
116
|
+
|
|
117
|
+
const get1 = await fetch('http://localhost:3849/storage/get/key1');
|
|
118
|
+
const d1 = await get1.json();
|
|
119
|
+
expect(d1.value).toBe('value1');
|
|
120
|
+
|
|
121
|
+
const get2 = await fetch('http://localhost:3849/storage/get/key2');
|
|
122
|
+
const d2 = await get2.json();
|
|
123
|
+
expect(d2.value).toBe('value2');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle batch-get with entityId', async () => {
|
|
127
|
+
await fetch('http://localhost:3849/storage/set/user-key1?entityId=user-1', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ value: 'user1-value1' }),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const response = await fetch('http://localhost:3849/storage/batch-get?entityId=user-1', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ keys: ['user-key1'] }),
|
|
137
|
+
});
|
|
138
|
+
const data = await response.json();
|
|
139
|
+
|
|
140
|
+
expect(response.status).toBe(200);
|
|
141
|
+
expect(data.entries['user-key1']).toBe('user1-value1');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle batch-set with entityId', async () => {
|
|
145
|
+
const response = await fetch('http://localhost:3849/storage/batch-set?entityId=user-2', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
body: JSON.stringify({ entries: { 'user-key': 'user2-value' } }),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(response.status).toBe(200);
|
|
152
|
+
|
|
153
|
+
const get = await fetch('http://localhost:3849/storage/get/user-key?entityId=user-2');
|
|
154
|
+
const d = await get.json();
|
|
155
|
+
expect(d.value).toBe('user2-value');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { jsonlStorage } from '../../src/storage/jsonl';
|
|
5
|
+
|
|
6
|
+
describe('jsonlStorage', () => {
|
|
7
|
+
const testDir = path.join(__dirname, '../../.test-jsonl');
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
if (fs.existsSync(testDir)) {
|
|
11
|
+
fs.rmSync(testDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (fs.existsSync(testDir)) {
|
|
17
|
+
fs.rmSync(testDir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('without entityId set', () => {
|
|
22
|
+
it('should return null for get when entityId not set', async () => {
|
|
23
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
24
|
+
const result = await adapter.storage.get('key');
|
|
25
|
+
expect(result).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('with entityId set', () => {
|
|
30
|
+
it('should return null for non-existent key', async () => {
|
|
31
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
32
|
+
adapter.setEntityId('entity1');
|
|
33
|
+
const result = await adapter.storage.get('nonexistent');
|
|
34
|
+
expect(result).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should set and get a value', async () => {
|
|
38
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
39
|
+
adapter.setEntityId('entity1');
|
|
40
|
+
await adapter.storage.set('key', 'value');
|
|
41
|
+
const result = await adapter.storage.get('key');
|
|
42
|
+
expect(result).toBe('value');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should get the latest value', async () => {
|
|
46
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
47
|
+
adapter.setEntityId('entity1');
|
|
48
|
+
|
|
49
|
+
await adapter.storage.set('key', 'value1');
|
|
50
|
+
await adapter.storage.set('key', 'value2');
|
|
51
|
+
await adapter.storage.set('key', 'value3');
|
|
52
|
+
|
|
53
|
+
const result = await adapter.storage.get('key');
|
|
54
|
+
expect(result).toBe('value3');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should remove a key', async () => {
|
|
58
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
59
|
+
adapter.setEntityId('entity1');
|
|
60
|
+
|
|
61
|
+
await adapter.storage.set('key', 'value');
|
|
62
|
+
await adapter.storage.remove('key');
|
|
63
|
+
|
|
64
|
+
const result = await adapter.storage.get('key');
|
|
65
|
+
expect(result).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle multiple entityIds', async () => {
|
|
69
|
+
const adapter1 = jsonlStorage({ dir: testDir });
|
|
70
|
+
const adapter2 = jsonlStorage({ dir: testDir });
|
|
71
|
+
|
|
72
|
+
adapter1.setEntityId('entity1');
|
|
73
|
+
adapter2.setEntityId('entity2');
|
|
74
|
+
|
|
75
|
+
await adapter1.storage.set('key', 'value1');
|
|
76
|
+
await adapter2.storage.set('key', 'value2');
|
|
77
|
+
|
|
78
|
+
expect(await adapter1.storage.get('key')).toBe('value1');
|
|
79
|
+
expect(await adapter2.storage.get('key')).toBe('value2');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should sanitize key in filename', async () => {
|
|
83
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
84
|
+
adapter.setEntityId('entity1');
|
|
85
|
+
|
|
86
|
+
await adapter.storage.set('key with spaces', 'value');
|
|
87
|
+
|
|
88
|
+
const filePath = path.join(testDir, 'entity1', 'key_with_spaces.jsonl');
|
|
89
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle multiple types', async () => {
|
|
93
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
94
|
+
adapter.setEntityId('entity1');
|
|
95
|
+
|
|
96
|
+
await adapter.storage.set('string', 'hello');
|
|
97
|
+
await adapter.storage.set('number', 42);
|
|
98
|
+
await adapter.storage.set('boolean', true);
|
|
99
|
+
await adapter.storage.set('array', [1, 2, 3]);
|
|
100
|
+
await adapter.storage.set('object', { a: 1 });
|
|
101
|
+
|
|
102
|
+
expect(await adapter.storage.get('string')).toBe('hello');
|
|
103
|
+
expect(await adapter.storage.get('number')).toBe(42);
|
|
104
|
+
expect(await adapter.storage.get('boolean')).toBe(true);
|
|
105
|
+
expect(await adapter.storage.get('array')).toEqual([1, 2, 3]);
|
|
106
|
+
expect(await adapter.storage.get('object')).toEqual({ a: 1 });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('getMultiple', () => {
|
|
110
|
+
it('should return multiple values', async () => {
|
|
111
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
112
|
+
adapter.setEntityId('entity1');
|
|
113
|
+
|
|
114
|
+
await adapter.storage.set('key1', 'value1');
|
|
115
|
+
await adapter.storage.set('key2', 'value2');
|
|
116
|
+
await adapter.storage.set('key3', 'value3');
|
|
117
|
+
|
|
118
|
+
const result = await adapter.storage.getMultiple(['key1', 'key2', 'key3']);
|
|
119
|
+
|
|
120
|
+
expect(result.size).toBe(3);
|
|
121
|
+
expect(result.get('key1')).toBe('value1');
|
|
122
|
+
expect(result.get('key2')).toBe('value2');
|
|
123
|
+
expect(result.get('key3')).toBe('value3');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should have correct name', () => {
|
|
128
|
+
const adapter = jsonlStorage({ dir: testDir });
|
|
129
|
+
expect(adapter.name).toBe('jsonl');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { memoryStorage } from '../../src/storage/memory';
|
|
3
|
+
|
|
4
|
+
describe('memoryStorage', () => {
|
|
5
|
+
it('should return null for non-existent key', async () => {
|
|
6
|
+
const adapter = memoryStorage();
|
|
7
|
+
const result = await adapter.storage.get('nonexistent');
|
|
8
|
+
expect(result).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should set and get a value', async () => {
|
|
12
|
+
const adapter = memoryStorage();
|
|
13
|
+
await adapter.storage.set('key', 'value');
|
|
14
|
+
const result = await adapter.storage.get('key');
|
|
15
|
+
expect(result).toBe('value');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should overwrite existing value', async () => {
|
|
19
|
+
const adapter = memoryStorage();
|
|
20
|
+
await adapter.storage.set('key', 'value1');
|
|
21
|
+
await adapter.storage.set('key', 'value2');
|
|
22
|
+
const result = await adapter.storage.get('key');
|
|
23
|
+
expect(result).toBe('value2');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should remove a value', async () => {
|
|
27
|
+
const adapter = memoryStorage();
|
|
28
|
+
await adapter.storage.set('key', 'value');
|
|
29
|
+
await adapter.storage.remove('key');
|
|
30
|
+
const result = await adapter.storage.get('key');
|
|
31
|
+
expect(result).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should handle multiple types', async () => {
|
|
35
|
+
const adapter = memoryStorage();
|
|
36
|
+
|
|
37
|
+
await adapter.storage.set('string', 'hello');
|
|
38
|
+
await adapter.storage.set('number', 42);
|
|
39
|
+
await adapter.storage.set('boolean', true);
|
|
40
|
+
await adapter.storage.set('array', [1, 2, 3]);
|
|
41
|
+
await adapter.storage.set('object', { a: 1 });
|
|
42
|
+
|
|
43
|
+
expect(await adapter.storage.get('string')).toBe('hello');
|
|
44
|
+
expect(await adapter.storage.get('number')).toBe(42);
|
|
45
|
+
expect(await adapter.storage.get('boolean')).toBe(true);
|
|
46
|
+
expect(await adapter.storage.get('array')).toEqual([1, 2, 3]);
|
|
47
|
+
expect(await adapter.storage.get('object')).toEqual({ a: 1 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getMultiple', () => {
|
|
51
|
+
it('should return multiple values', async () => {
|
|
52
|
+
const adapter = memoryStorage();
|
|
53
|
+
await adapter.storage.set('key1', 'value1');
|
|
54
|
+
await adapter.storage.set('key2', 'value2');
|
|
55
|
+
await adapter.storage.set('key3', 'value3');
|
|
56
|
+
|
|
57
|
+
const result = await adapter.storage.getMultiple(['key1', 'key2', 'key3']);
|
|
58
|
+
|
|
59
|
+
expect(result.size).toBe(3);
|
|
60
|
+
expect(result.get('key1')).toBe('value1');
|
|
61
|
+
expect(result.get('key2')).toBe('value2');
|
|
62
|
+
expect(result.get('key3')).toBe('value3');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should skip non-existent keys', async () => {
|
|
66
|
+
const adapter = memoryStorage();
|
|
67
|
+
await adapter.storage.set('key1', 'value1');
|
|
68
|
+
|
|
69
|
+
const result = await adapter.storage.getMultiple(['key1', 'nonexistent']);
|
|
70
|
+
|
|
71
|
+
expect(result.size).toBe(1);
|
|
72
|
+
expect(result.get('key1')).toBe('value1');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return empty map for empty input', async () => {
|
|
76
|
+
const adapter = memoryStorage();
|
|
77
|
+
const result = await adapter.storage.getMultiple([]);
|
|
78
|
+
expect(result.size).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('setMultiple', () => {
|
|
83
|
+
it('should set multiple values', async () => {
|
|
84
|
+
const adapter = memoryStorage();
|
|
85
|
+
const entries = new Map([
|
|
86
|
+
['key1', 'value1'],
|
|
87
|
+
['key2', 'value2'],
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
await adapter.storage.setMultiple(entries);
|
|
91
|
+
|
|
92
|
+
expect(await adapter.storage.get('key1')).toBe('value1');
|
|
93
|
+
expect(await adapter.storage.get('key2')).toBe('value2');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should have correct name', () => {
|
|
98
|
+
const adapter = memoryStorage();
|
|
99
|
+
expect(adapter.name).toBe('memory');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { Storage, StorageAdapter } from '../../src/storage/base';
|
|
3
|
+
|
|
4
|
+
describe('mongodbStorage interface contract', () => {
|
|
5
|
+
describe('Storage interface', () => {
|
|
6
|
+
it('should define complete storage interface', () => {
|
|
7
|
+
const storageInterface: Storage = {
|
|
8
|
+
get: async <T>(_key: string): Promise<T | null> => null,
|
|
9
|
+
set: async <T>(_key: string, _value: T): Promise<void> => {},
|
|
10
|
+
remove: async (_key: string): Promise<void> => {},
|
|
11
|
+
getMultiple: async <T>(_keys: string[]): Promise<Map<string, T>> => new Map(),
|
|
12
|
+
setMultiple: async <T>(_entries: Map<string, T>): Promise<void> => {},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
expect(typeof storageInterface.get).toBe('function');
|
|
16
|
+
expect(typeof storageInterface.set).toBe('function');
|
|
17
|
+
expect(typeof storageInterface.remove).toBe('function');
|
|
18
|
+
expect(typeof storageInterface.getMultiple).toBe('function');
|
|
19
|
+
expect(typeof storageInterface.setMultiple).toBe('function');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('StorageAdapter interface', () => {
|
|
24
|
+
it('should define storage adapter with name and storage', () => {
|
|
25
|
+
const adapter: StorageAdapter = {
|
|
26
|
+
storage: {
|
|
27
|
+
get: async <T>(_key: string): Promise<T | null> => null,
|
|
28
|
+
set: async <T>(_key: string, _value: T): Promise<void> => {},
|
|
29
|
+
remove: async (_key: string): Promise<void> => {},
|
|
30
|
+
},
|
|
31
|
+
name: 'mongodb',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
expect(adapter.name).toBe('mongodb');
|
|
35
|
+
expect(adapter.storage).toBeDefined();
|
|
36
|
+
expect(typeof adapter.storage.get).toBe('function');
|
|
37
|
+
expect(typeof adapter.storage.set).toBe('function');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { remoteStorage } from '../../src/storage/adapters/remoteStorage';
|
|
3
|
+
|
|
4
|
+
describe('remoteStorage', () => {
|
|
5
|
+
const originalFetch = globalThis.fetch;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('basic creation', () => {
|
|
12
|
+
it('should create adapter with default baseUrl', () => {
|
|
13
|
+
const adapter = remoteStorage();
|
|
14
|
+
expect(adapter.name).toBe('remote');
|
|
15
|
+
expect(adapter.storage).toBeDefined();
|
|
16
|
+
expect(typeof adapter.storage.get).toBe('function');
|
|
17
|
+
expect(typeof adapter.storage.set).toBe('function');
|
|
18
|
+
expect(typeof adapter.storage.remove).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should create adapter with custom baseUrl', () => {
|
|
22
|
+
const adapter = remoteStorage({ baseUrl: 'http://custom:9999' });
|
|
23
|
+
expect(adapter.name).toBe('remote');
|
|
24
|
+
expect(adapter.storage).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should implement setEntityId', () => {
|
|
28
|
+
const adapter = remoteStorage();
|
|
29
|
+
expect(typeof adapter.setEntityId).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('storage operations', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
globalThis.fetch = vi.fn();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
globalThis.fetch = originalFetch;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should call fetch for get', async () => {
|
|
43
|
+
const mockResponse = {
|
|
44
|
+
ok: true,
|
|
45
|
+
statusText: 'OK',
|
|
46
|
+
json: vi.fn().mockResolvedValue({ value: 'test-value' }),
|
|
47
|
+
};
|
|
48
|
+
(globalThis.fetch as any).mockResolvedValue(mockResponse);
|
|
49
|
+
|
|
50
|
+
const adapter = remoteStorage({ baseUrl: 'http://localhost:3847' });
|
|
51
|
+
const result = await adapter.storage.get('theme');
|
|
52
|
+
|
|
53
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('http://localhost:3847/storage/get/theme');
|
|
54
|
+
expect(result).toBe('test-value');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should call fetch for set with POST', async () => {
|
|
58
|
+
const mockResponse = {
|
|
59
|
+
ok: true,
|
|
60
|
+
json: vi.fn().mockResolvedValue({ success: true }),
|
|
61
|
+
};
|
|
62
|
+
(globalThis.fetch as any).mockResolvedValue(mockResponse);
|
|
63
|
+
|
|
64
|
+
const adapter = remoteStorage();
|
|
65
|
+
await adapter.storage.set('theme', 'dark');
|
|
66
|
+
|
|
67
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
68
|
+
'http://localhost:3847/storage/set/theme',
|
|
69
|
+
expect.objectContaining({ method: 'POST' })
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should call fetch for remove with DELETE', async () => {
|
|
74
|
+
const mockResponse = {
|
|
75
|
+
ok: true,
|
|
76
|
+
json: vi.fn().mockResolvedValue({ success: true }),
|
|
77
|
+
};
|
|
78
|
+
(globalThis.fetch as any).mockResolvedValue(mockResponse);
|
|
79
|
+
|
|
80
|
+
const adapter = remoteStorage();
|
|
81
|
+
await adapter.storage.remove('theme');
|
|
82
|
+
|
|
83
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
84
|
+
'http://localhost:3847/storage/delete/theme',
|
|
85
|
+
expect.objectContaining({ method: 'DELETE' })
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle getMultiple', async () => {
|
|
90
|
+
const mockResponse = {
|
|
91
|
+
ok: true,
|
|
92
|
+
json: vi.fn().mockResolvedValue({
|
|
93
|
+
entries: { key1: 'value1', key2: 'value2' },
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
(globalThis.fetch as any).mockResolvedValue(mockResponse);
|
|
97
|
+
|
|
98
|
+
const adapter = remoteStorage();
|
|
99
|
+
const result = await adapter.storage.getMultiple(['key1', 'key2']);
|
|
100
|
+
|
|
101
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
102
|
+
'http://localhost:3847/storage/batch-get',
|
|
103
|
+
expect.objectContaining({ method: 'POST' })
|
|
104
|
+
);
|
|
105
|
+
expect(result.get('key1')).toBe('value1');
|
|
106
|
+
expect(result.get('key2')).toBe('value2');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle setMultiple', async () => {
|
|
110
|
+
const mockResponse = {
|
|
111
|
+
ok: true,
|
|
112
|
+
json: vi.fn().mockResolvedValue({ success: true }),
|
|
113
|
+
};
|
|
114
|
+
(globalThis.fetch as any).mockResolvedValue(mockResponse);
|
|
115
|
+
|
|
116
|
+
const adapter = remoteStorage();
|
|
117
|
+
const entries = new Map([['key1', 'value1'], ['key2', 'value2']]);
|
|
118
|
+
await adapter.storage.setMultiple(entries);
|
|
119
|
+
|
|
120
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
121
|
+
'http://localhost:3847/storage/batch-set',
|
|
122
|
+
expect.objectContaining({ method: 'POST' })
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|