pluresdb 1.0.1
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 +72 -0
- package/README.md +322 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +253 -0
- package/dist/cli.js.map +1 -0
- package/dist/node-index.d.ts +52 -0
- package/dist/node-index.d.ts.map +1 -0
- package/dist/node-index.js +359 -0
- package/dist/node-index.js.map +1 -0
- package/dist/node-wrapper.d.ts +44 -0
- package/dist/node-wrapper.d.ts.map +1 -0
- package/dist/node-wrapper.js +294 -0
- package/dist/node-wrapper.js.map +1 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/node-types.d.ts +59 -0
- package/dist/types/node-types.d.ts.map +1 -0
- package/dist/types/node-types.js +6 -0
- package/dist/types/node-types.js.map +1 -0
- package/dist/vscode/extension.d.ts +81 -0
- package/dist/vscode/extension.d.ts.map +1 -0
- package/dist/vscode/extension.js +309 -0
- package/dist/vscode/extension.js.map +1 -0
- package/examples/basic-usage.d.ts +2 -0
- package/examples/basic-usage.d.ts.map +1 -0
- package/examples/basic-usage.js +26 -0
- package/examples/basic-usage.js.map +1 -0
- package/examples/basic-usage.ts +29 -0
- package/examples/vscode-extension-example/README.md +95 -0
- package/examples/vscode-extension-example/package.json +49 -0
- package/examples/vscode-extension-example/src/extension.ts +163 -0
- package/examples/vscode-extension-example/tsconfig.json +12 -0
- package/examples/vscode-extension-integration.d.ts +24 -0
- package/examples/vscode-extension-integration.d.ts.map +1 -0
- package/examples/vscode-extension-integration.js +285 -0
- package/examples/vscode-extension-integration.js.map +1 -0
- package/examples/vscode-extension-integration.ts +41 -0
- package/package.json +115 -0
- package/scripts/compiled-crud-verify.ts +28 -0
- package/scripts/dogfood.ts +258 -0
- package/scripts/postinstall.js +155 -0
- package/scripts/run-tests.ts +175 -0
- package/scripts/setup-libclang.ps1 +209 -0
- package/src/benchmarks/memory-benchmarks.ts +316 -0
- package/src/benchmarks/run-benchmarks.ts +293 -0
- package/src/cli.ts +231 -0
- package/src/config.ts +49 -0
- package/src/core/crdt.ts +104 -0
- package/src/core/database.ts +494 -0
- package/src/healthcheck.ts +156 -0
- package/src/http/api-server.ts +334 -0
- package/src/index.ts +28 -0
- package/src/logic/rules.ts +44 -0
- package/src/main.rs +3 -0
- package/src/main.ts +190 -0
- package/src/network/websocket-server.ts +115 -0
- package/src/node-index.ts +385 -0
- package/src/node-wrapper.ts +320 -0
- package/src/sqlite-compat.ts +586 -0
- package/src/sqlite3-compat.ts +55 -0
- package/src/storage/kv-storage.ts +71 -0
- package/src/tests/core.test.ts +281 -0
- package/src/tests/fixtures/performance-data.json +71 -0
- package/src/tests/fixtures/test-data.json +124 -0
- package/src/tests/integration/api-server.test.ts +232 -0
- package/src/tests/integration/mesh-network.test.ts +297 -0
- package/src/tests/logic.test.ts +30 -0
- package/src/tests/performance/load.test.ts +288 -0
- package/src/tests/security/input-validation.test.ts +282 -0
- package/src/tests/unit/core.test.ts +216 -0
- package/src/tests/unit/subscriptions.test.ts +135 -0
- package/src/tests/unit/vector-search.test.ts +173 -0
- package/src/tests/vscode_extension_test.ts +253 -0
- package/src/types/index.ts +32 -0
- package/src/types/node-types.ts +66 -0
- package/src/util/debug.ts +14 -0
- package/src/vector/index.ts +59 -0
- package/src/vscode/extension.ts +364 -0
- package/web/README.md +27 -0
- package/web/svelte/package.json +31 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert@1.0.14";
|
|
3
|
+
import { GunDB } from "../../core/database.ts";
|
|
4
|
+
|
|
5
|
+
Deno.test("Security - SQL Injection Prevention", async () => {
|
|
6
|
+
const db = new GunDB();
|
|
7
|
+
try {
|
|
8
|
+
const kvPath = await Deno.makeTempFile({
|
|
9
|
+
prefix: "kv_",
|
|
10
|
+
suffix: ".sqlite",
|
|
11
|
+
});
|
|
12
|
+
await db.ready(kvPath);
|
|
13
|
+
|
|
14
|
+
// Test malicious SQL injection attempts
|
|
15
|
+
const maliciousInputs = [
|
|
16
|
+
"'; DROP TABLE users; --",
|
|
17
|
+
"' OR '1'='1",
|
|
18
|
+
"'; INSERT INTO users VALUES ('hacker', 'password'); --",
|
|
19
|
+
"'; UPDATE users SET password='hacked'; --",
|
|
20
|
+
"'; DELETE FROM users; --",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
for (const maliciousInput of maliciousInputs) {
|
|
24
|
+
// These should be treated as regular string data, not executed as SQL
|
|
25
|
+
await db.put("test:sql", {
|
|
26
|
+
malicious: maliciousInput,
|
|
27
|
+
safe: "normal data",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const result = await db.get("test:sql");
|
|
31
|
+
assertExists(result);
|
|
32
|
+
assertEquals((result as any).malicious, maliciousInput);
|
|
33
|
+
assertEquals((result as any).safe, "normal data");
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
await db.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
Deno.test("Security - XSS Prevention", async () => {
|
|
41
|
+
const db = new GunDB();
|
|
42
|
+
try {
|
|
43
|
+
const kvPath = await Deno.makeTempFile({
|
|
44
|
+
prefix: "kv_",
|
|
45
|
+
suffix: ".sqlite",
|
|
46
|
+
});
|
|
47
|
+
await db.ready(kvPath);
|
|
48
|
+
|
|
49
|
+
// Test XSS payloads
|
|
50
|
+
const xssPayloads = [
|
|
51
|
+
"<script>alert('xss')</script>",
|
|
52
|
+
"javascript:alert('xss')",
|
|
53
|
+
"<img src=x onerror=alert('xss')>",
|
|
54
|
+
"<svg onload=alert('xss')>",
|
|
55
|
+
"';alert('xss');//",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const payload of xssPayloads) {
|
|
59
|
+
await db.put("test:xss", {
|
|
60
|
+
payload: payload,
|
|
61
|
+
content: "Safe content",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const result = await db.get("test:xss");
|
|
65
|
+
assertExists(result);
|
|
66
|
+
// Data should be stored as-is without interpretation
|
|
67
|
+
assertEquals((result as any).payload, payload);
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
await db.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
Deno.test("Security - Path Traversal Prevention", async () => {
|
|
75
|
+
const db = new GunDB();
|
|
76
|
+
try {
|
|
77
|
+
const kvPath = await Deno.makeTempFile({
|
|
78
|
+
prefix: "kv_",
|
|
79
|
+
suffix: ".sqlite",
|
|
80
|
+
});
|
|
81
|
+
await db.ready(kvPath);
|
|
82
|
+
|
|
83
|
+
// Test path traversal attempts
|
|
84
|
+
const pathTraversalAttempts = [
|
|
85
|
+
"../../../etc/passwd",
|
|
86
|
+
"..\\..\\..\\windows\\system32\\drivers\\etc\\hosts",
|
|
87
|
+
"/etc/passwd",
|
|
88
|
+
"C:\\Windows\\System32\\config\\SAM",
|
|
89
|
+
"....//....//....//etc//passwd",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const path of pathTraversalAttempts) {
|
|
93
|
+
// These should be treated as regular key names, not file paths
|
|
94
|
+
await db.put(path, {
|
|
95
|
+
content: "This should not access filesystem",
|
|
96
|
+
path: path,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const result = await db.get(path);
|
|
100
|
+
assertExists(result);
|
|
101
|
+
assertEquals((result as any).path, path);
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
await db.close();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
Deno.test("Security - Large Payload Prevention", async () => {
|
|
109
|
+
const db = new GunDB();
|
|
110
|
+
try {
|
|
111
|
+
const kvPath = await Deno.makeTempFile({
|
|
112
|
+
prefix: "kv_",
|
|
113
|
+
suffix: ".sqlite",
|
|
114
|
+
});
|
|
115
|
+
await db.ready(kvPath);
|
|
116
|
+
|
|
117
|
+
// Test very large payloads
|
|
118
|
+
const largePayload = "x".repeat(10 * 1024 * 1024); // 10MB
|
|
119
|
+
|
|
120
|
+
await assertRejects(
|
|
121
|
+
async () => {
|
|
122
|
+
await db.put("test:large", {
|
|
123
|
+
data: largePayload,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
Error,
|
|
127
|
+
"Value too large",
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
await db.close();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
Deno.test("Security - Malformed JSON Handling", async () => {
|
|
135
|
+
const db = new GunDB();
|
|
136
|
+
try {
|
|
137
|
+
const kvPath = await Deno.makeTempFile({
|
|
138
|
+
prefix: "kv_",
|
|
139
|
+
suffix: ".sqlite",
|
|
140
|
+
});
|
|
141
|
+
await db.ready(kvPath);
|
|
142
|
+
|
|
143
|
+
// Test malformed JSON inputs
|
|
144
|
+
const malformedInputs = [
|
|
145
|
+
"{ invalid json }",
|
|
146
|
+
'{ "incomplete": ',
|
|
147
|
+
'{ "nested": { "broken": } }',
|
|
148
|
+
'{ "array": [1, 2, } }',
|
|
149
|
+
'{ "string": "unclosed }',
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
for (const malformed of malformedInputs) {
|
|
153
|
+
await db.put("test:malformed", {
|
|
154
|
+
json: malformed,
|
|
155
|
+
});
|
|
156
|
+
const stored = await db.get("test:malformed");
|
|
157
|
+
assertExists(stored);
|
|
158
|
+
assertEquals((stored as any).json, malformed);
|
|
159
|
+
}
|
|
160
|
+
} finally {
|
|
161
|
+
await db.close();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
Deno.test("Security - Type Confusion Prevention", async () => {
|
|
166
|
+
const db = new GunDB();
|
|
167
|
+
try {
|
|
168
|
+
const kvPath = await Deno.makeTempFile({
|
|
169
|
+
prefix: "kv_",
|
|
170
|
+
suffix: ".sqlite",
|
|
171
|
+
});
|
|
172
|
+
await db.ready(kvPath);
|
|
173
|
+
|
|
174
|
+
// Test type confusion attempts
|
|
175
|
+
const typeConfusionAttempts = [
|
|
176
|
+
{ __proto__: { isAdmin: true } },
|
|
177
|
+
{ constructor: { prototype: { isAdmin: true } } },
|
|
178
|
+
{ toString: () => "hacked" },
|
|
179
|
+
{ valueOf: () => 999999 },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const attempt of typeConfusionAttempts) {
|
|
183
|
+
await db.put("test:type", attempt);
|
|
184
|
+
|
|
185
|
+
const result = await db.get("test:type");
|
|
186
|
+
assertExists(result);
|
|
187
|
+
|
|
188
|
+
// Should not have inherited properties
|
|
189
|
+
assertEquals((result as any).isAdmin, undefined);
|
|
190
|
+
assertEquals(typeof (result as any).toString, "string");
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
await db.close();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
Deno.test("Security - Vector Search Injection", async () => {
|
|
198
|
+
const db = new GunDB();
|
|
199
|
+
try {
|
|
200
|
+
const kvPath = await Deno.makeTempFile({
|
|
201
|
+
prefix: "kv_",
|
|
202
|
+
suffix: ".sqlite",
|
|
203
|
+
});
|
|
204
|
+
await db.ready(kvPath);
|
|
205
|
+
|
|
206
|
+
// Test vector search with malicious inputs
|
|
207
|
+
const maliciousQueries = [
|
|
208
|
+
"'; DROP TABLE nodes; --",
|
|
209
|
+
"<script>alert('xss')</script>",
|
|
210
|
+
"../../../etc/passwd",
|
|
211
|
+
"'; INSERT INTO nodes VALUES ('hacked', 'data'); --",
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
for (const query of maliciousQueries) {
|
|
215
|
+
// Vector search should handle these safely
|
|
216
|
+
const results = await db.vectorSearch(query, 5);
|
|
217
|
+
assertEquals(Array.isArray(results), true);
|
|
218
|
+
// Should not throw errors or execute malicious code
|
|
219
|
+
}
|
|
220
|
+
} finally {
|
|
221
|
+
await db.close();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
Deno.test("Security - Subscription Injection", async () => {
|
|
226
|
+
const db = new GunDB();
|
|
227
|
+
try {
|
|
228
|
+
const kvPath = await Deno.makeTempFile({
|
|
229
|
+
prefix: "kv_",
|
|
230
|
+
suffix: ".sqlite",
|
|
231
|
+
});
|
|
232
|
+
await db.ready(kvPath);
|
|
233
|
+
|
|
234
|
+
// Test subscription with malicious IDs
|
|
235
|
+
const maliciousIds = [
|
|
236
|
+
"'; DROP TABLE nodes; --",
|
|
237
|
+
"<script>alert('xss')</script>",
|
|
238
|
+
"../../../etc/passwd",
|
|
239
|
+
"'; INSERT INTO nodes VALUES ('hacked', 'data'); --",
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
for (const id of maliciousIds) {
|
|
243
|
+
// Subscriptions should handle these safely
|
|
244
|
+
const unsubscribe = db.on(id, () => {});
|
|
245
|
+
assertExists(unsubscribe);
|
|
246
|
+
|
|
247
|
+
// Should be able to unsubscribe safely
|
|
248
|
+
unsubscribe();
|
|
249
|
+
}
|
|
250
|
+
} finally {
|
|
251
|
+
await db.close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
Deno.test("Security - Memory Exhaustion Prevention", async () => {
|
|
256
|
+
const db = new GunDB();
|
|
257
|
+
try {
|
|
258
|
+
const kvPath = await Deno.makeTempFile({
|
|
259
|
+
prefix: "kv_",
|
|
260
|
+
suffix: ".sqlite",
|
|
261
|
+
});
|
|
262
|
+
await db.ready(kvPath);
|
|
263
|
+
|
|
264
|
+
// Test creating many subscriptions to exhaust memory
|
|
265
|
+
const subscriptions: Array<() => void> = [];
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
for (let i = 0; i < 100000; i++) {
|
|
269
|
+
const unsubscribe = db.on(`memory:${i}`, () => {});
|
|
270
|
+
subscriptions.push(unsubscribe);
|
|
271
|
+
}
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Should fail gracefully with memory limit
|
|
274
|
+
assertEquals(error.message.includes("Memory limit"), true);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Clean up any created subscriptions
|
|
278
|
+
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
279
|
+
} finally {
|
|
280
|
+
await db.close();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert@1.0.14";
|
|
3
|
+
import { GunDB } from "../../core/database.ts";
|
|
4
|
+
import { mergeNodes } from "../../core/crdt.ts";
|
|
5
|
+
import type { NodeRecord } from "../../types/index.ts";
|
|
6
|
+
|
|
7
|
+
Deno.test("Core Database - Basic CRUD Operations", async () => {
|
|
8
|
+
const db = new GunDB();
|
|
9
|
+
try {
|
|
10
|
+
const kvPath = await Deno.makeTempFile({
|
|
11
|
+
prefix: "kv_",
|
|
12
|
+
suffix: ".sqlite",
|
|
13
|
+
});
|
|
14
|
+
await db.ready(kvPath);
|
|
15
|
+
|
|
16
|
+
// Test put and get
|
|
17
|
+
const user = { name: "Alice", age: 30, email: "alice@example.com" };
|
|
18
|
+
await db.put("user:alice", user as unknown as Record<string, unknown>);
|
|
19
|
+
const got = await db.get<typeof user>("user:alice");
|
|
20
|
+
|
|
21
|
+
assertEquals(got?.name, "Alice");
|
|
22
|
+
assertEquals(got?.age, 30);
|
|
23
|
+
assertEquals(got?.email, "alice@example.com");
|
|
24
|
+
|
|
25
|
+
// Test update
|
|
26
|
+
await db.put("user:alice", { ...user, age: 31 });
|
|
27
|
+
const updated = await db.get<typeof user>("user:alice");
|
|
28
|
+
assertEquals(updated?.age, 31);
|
|
29
|
+
|
|
30
|
+
// Test delete
|
|
31
|
+
await db.delete("user:alice");
|
|
32
|
+
const deleted = await db.get<typeof user>("user:alice");
|
|
33
|
+
assertEquals(deleted, null);
|
|
34
|
+
} finally {
|
|
35
|
+
await db.close();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
Deno.test("Core Database - Vector Clock Increments", async () => {
|
|
40
|
+
const db = new GunDB();
|
|
41
|
+
try {
|
|
42
|
+
const kvPath = await Deno.makeTempFile({
|
|
43
|
+
prefix: "kv_",
|
|
44
|
+
suffix: ".sqlite",
|
|
45
|
+
});
|
|
46
|
+
await db.ready(kvPath);
|
|
47
|
+
|
|
48
|
+
const id = "vc:test";
|
|
49
|
+
await db.put(id, { value: 1 });
|
|
50
|
+
await db.put(id, { value: 2 });
|
|
51
|
+
await db.put(id, { value: 3 });
|
|
52
|
+
|
|
53
|
+
// Inspect underlying record
|
|
54
|
+
const { KvStorage } = await import("../../storage/kv-storage.ts");
|
|
55
|
+
const kv = new KvStorage();
|
|
56
|
+
await kv.open(kvPath);
|
|
57
|
+
const node = await kv.getNode(id);
|
|
58
|
+
await kv.close();
|
|
59
|
+
|
|
60
|
+
assertExists(node);
|
|
61
|
+
const clockValues = Object.values(node.vectorClock);
|
|
62
|
+
assertEquals(clockValues.length, 1);
|
|
63
|
+
assertEquals(clockValues[0], 3);
|
|
64
|
+
} finally {
|
|
65
|
+
await db.close();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
Deno.test("Core Database - Type System", async () => {
|
|
70
|
+
const db = new GunDB();
|
|
71
|
+
try {
|
|
72
|
+
const kvPath = await Deno.makeTempFile({
|
|
73
|
+
prefix: "kv_",
|
|
74
|
+
suffix: ".sqlite",
|
|
75
|
+
});
|
|
76
|
+
await db.ready(kvPath);
|
|
77
|
+
|
|
78
|
+
// Test setType and instancesOf
|
|
79
|
+
await db.put("person:1", { name: "Alice" });
|
|
80
|
+
await db.setType("person:1", "Person");
|
|
81
|
+
|
|
82
|
+
await db.put("company:1", { name: "Acme Corp" });
|
|
83
|
+
await db.setType("company:1", "Company");
|
|
84
|
+
|
|
85
|
+
const people = await db.instancesOf("Person");
|
|
86
|
+
assertEquals(people.length, 1);
|
|
87
|
+
assertEquals(people[0].id, "person:1");
|
|
88
|
+
|
|
89
|
+
const companies = await db.instancesOf("Company");
|
|
90
|
+
assertEquals(companies.length, 1);
|
|
91
|
+
assertEquals(companies[0].id, "company:1");
|
|
92
|
+
} finally {
|
|
93
|
+
await db.close();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Deno.test("Core Database - Vector Search", async () => {
|
|
98
|
+
const db = new GunDB();
|
|
99
|
+
try {
|
|
100
|
+
const kvPath = await Deno.makeTempFile({
|
|
101
|
+
prefix: "kv_",
|
|
102
|
+
suffix: ".sqlite",
|
|
103
|
+
});
|
|
104
|
+
await db.ready(kvPath);
|
|
105
|
+
|
|
106
|
+
// Add documents with text content
|
|
107
|
+
await db.put("doc:1", { text: "Machine learning and artificial intelligence" });
|
|
108
|
+
await db.put("doc:2", { text: "Cooking recipes and food preparation" });
|
|
109
|
+
await db.put("doc:3", { text: "Deep learning neural networks" });
|
|
110
|
+
|
|
111
|
+
// Test vector search
|
|
112
|
+
const results = await db.vectorSearch("machine learning", 2);
|
|
113
|
+
assertExists(results);
|
|
114
|
+
assertEquals(results.length, 2);
|
|
115
|
+
|
|
116
|
+
// Should find the most relevant documents
|
|
117
|
+
const docIds = results.map((r) => r.id);
|
|
118
|
+
assertExists(docIds.includes("doc:1"));
|
|
119
|
+
assertExists(docIds.includes("doc:3"));
|
|
120
|
+
} finally {
|
|
121
|
+
await db.close();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
Deno.test("CRDT Merge - Equal Timestamps", () => {
|
|
126
|
+
const timestamp = Date.now();
|
|
127
|
+
const local: NodeRecord = {
|
|
128
|
+
id: "test:1",
|
|
129
|
+
data: { a: 1, shared: 1, nested: { x: 1, y: 1 } },
|
|
130
|
+
vector: [0.1, 0.2],
|
|
131
|
+
type: "TestType",
|
|
132
|
+
timestamp,
|
|
133
|
+
vectorClock: { peerA: 2 },
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const incoming: NodeRecord = {
|
|
137
|
+
id: "test:1",
|
|
138
|
+
data: { b: 2, shared: 2, nested: { y: 2, z: 3 } },
|
|
139
|
+
timestamp,
|
|
140
|
+
vectorClock: { peerB: 3 },
|
|
141
|
+
} as unknown as NodeRecord;
|
|
142
|
+
|
|
143
|
+
const merged = mergeNodes(local, incoming);
|
|
144
|
+
|
|
145
|
+
assertEquals(merged.id, "test:1");
|
|
146
|
+
assertEquals(merged.timestamp, timestamp);
|
|
147
|
+
assertEquals(merged.data, {
|
|
148
|
+
a: 1,
|
|
149
|
+
shared: 2,
|
|
150
|
+
b: 2,
|
|
151
|
+
nested: { x: 1, y: 2, z: 3 },
|
|
152
|
+
});
|
|
153
|
+
assertEquals(merged.type, "TestType");
|
|
154
|
+
assertEquals(merged.vector, [0.1, 0.2]);
|
|
155
|
+
assertEquals(merged.vectorClock.peerA, 2);
|
|
156
|
+
assertEquals(merged.vectorClock.peerB, 3);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
Deno.test("CRDT Merge - LWW on Different Timestamps", () => {
|
|
160
|
+
const t1 = 1000;
|
|
161
|
+
const t2 = 2000;
|
|
162
|
+
|
|
163
|
+
const older: NodeRecord = {
|
|
164
|
+
id: "test:2",
|
|
165
|
+
data: { a: 1, b: 1 },
|
|
166
|
+
timestamp: t1,
|
|
167
|
+
vectorClock: { p1: 1 },
|
|
168
|
+
} as unknown as NodeRecord;
|
|
169
|
+
|
|
170
|
+
const newer: NodeRecord = {
|
|
171
|
+
id: "test:2",
|
|
172
|
+
data: { a: 999, b: 2, c: 3 },
|
|
173
|
+
timestamp: t2,
|
|
174
|
+
vectorClock: { p2: 1 },
|
|
175
|
+
} as unknown as NodeRecord;
|
|
176
|
+
|
|
177
|
+
const merged = mergeNodes(older, newer);
|
|
178
|
+
|
|
179
|
+
assertEquals(merged.data, { a: 999, b: 2, c: 3 });
|
|
180
|
+
assertEquals(merged.timestamp, t2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
Deno.test("Core Database - Error Handling", async () => {
|
|
184
|
+
const db = new GunDB();
|
|
185
|
+
|
|
186
|
+
// Test operations before ready
|
|
187
|
+
await assertRejects(() => db.put("test", { value: 1 }), Error, "Database not ready");
|
|
188
|
+
|
|
189
|
+
await assertRejects(() => db.get("test"), Error, "Database not ready");
|
|
190
|
+
|
|
191
|
+
await assertRejects(() => db.delete("test"), Error, "Database not ready");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
Deno.test("Core Database - Persistence Across Restarts", async () => {
|
|
195
|
+
const kvPath = await Deno.makeTempFile({
|
|
196
|
+
prefix: "kv_",
|
|
197
|
+
suffix: ".sqlite",
|
|
198
|
+
});
|
|
199
|
+
const id = "persist:test";
|
|
200
|
+
|
|
201
|
+
// First session
|
|
202
|
+
const db1 = new GunDB();
|
|
203
|
+
await db1.ready(kvPath);
|
|
204
|
+
await db1.put(id, { value: 123, text: "persistent data" });
|
|
205
|
+
await db1.close();
|
|
206
|
+
|
|
207
|
+
// Second session
|
|
208
|
+
const db2 = new GunDB();
|
|
209
|
+
await db2.ready(kvPath);
|
|
210
|
+
const got = await db2.get<{ value: number; text: string }>(id);
|
|
211
|
+
await db2.close();
|
|
212
|
+
|
|
213
|
+
assertExists(got);
|
|
214
|
+
assertEquals(got.value, 123);
|
|
215
|
+
assertEquals(got.text, "persistent data");
|
|
216
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { assertEquals, assertThrows } from "jsr:@std/assert@1.0.14";
|
|
3
|
+
import { GunDB } from "../../core/database.ts";
|
|
4
|
+
|
|
5
|
+
Deno.test("Subscriptions - Basic Update Events", async () => {
|
|
6
|
+
const db = new GunDB();
|
|
7
|
+
try {
|
|
8
|
+
const kvPath = await Deno.makeTempFile({
|
|
9
|
+
prefix: "kv_",
|
|
10
|
+
suffix: ".sqlite",
|
|
11
|
+
});
|
|
12
|
+
await db.ready(kvPath);
|
|
13
|
+
|
|
14
|
+
const id = "user:test";
|
|
15
|
+
let updateCount = 0;
|
|
16
|
+
let lastData: any = null;
|
|
17
|
+
|
|
18
|
+
const unsubscribe = db.on(id, (node) => {
|
|
19
|
+
updateCount++;
|
|
20
|
+
lastData = node?.data;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Test initial put
|
|
24
|
+
await db.put(id, { name: "Alice", age: 30 });
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
26
|
+
assertEquals(updateCount, 1);
|
|
27
|
+
assertEquals(lastData?.name, "Alice");
|
|
28
|
+
|
|
29
|
+
// Test update
|
|
30
|
+
await db.put(id, { name: "Alice", age: 31 });
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
32
|
+
assertEquals(updateCount, 2);
|
|
33
|
+
assertEquals(lastData?.age, 31);
|
|
34
|
+
|
|
35
|
+
// Test unsubscribe
|
|
36
|
+
unsubscribe();
|
|
37
|
+
await db.put(id, { name: "Alice", age: 32 });
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
39
|
+
assertEquals(updateCount, 2); // Should not increment
|
|
40
|
+
} finally {
|
|
41
|
+
await db.close();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
Deno.test("Subscriptions - Delete Events", async () => {
|
|
46
|
+
const db = new GunDB();
|
|
47
|
+
try {
|
|
48
|
+
const kvPath = await Deno.makeTempFile({
|
|
49
|
+
prefix: "kv_",
|
|
50
|
+
suffix: ".sqlite",
|
|
51
|
+
});
|
|
52
|
+
await db.ready(kvPath);
|
|
53
|
+
|
|
54
|
+
const id = "user:delete";
|
|
55
|
+
let deleteReceived = false;
|
|
56
|
+
|
|
57
|
+
const unsubscribe = db.on(id, (node) => {
|
|
58
|
+
if (node === null) {
|
|
59
|
+
deleteReceived = true;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await db.put(id, { name: "Bob" });
|
|
64
|
+
await db.delete(id);
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
66
|
+
|
|
67
|
+
assertEquals(deleteReceived, true);
|
|
68
|
+
unsubscribe();
|
|
69
|
+
} finally {
|
|
70
|
+
await db.close();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
Deno.test("Subscriptions - Multiple Subscribers", async () => {
|
|
75
|
+
const db = new GunDB();
|
|
76
|
+
try {
|
|
77
|
+
const kvPath = await Deno.makeTempFile({
|
|
78
|
+
prefix: "kv_",
|
|
79
|
+
suffix: ".sqlite",
|
|
80
|
+
});
|
|
81
|
+
await db.ready(kvPath);
|
|
82
|
+
|
|
83
|
+
const id = "user:multi";
|
|
84
|
+
let subscriber1Count = 0;
|
|
85
|
+
let subscriber2Count = 0;
|
|
86
|
+
|
|
87
|
+
const unsubscribe1 = db.on(id, () => subscriber1Count++);
|
|
88
|
+
const unsubscribe2 = db.on(id, () => subscriber2Count++);
|
|
89
|
+
|
|
90
|
+
await db.put(id, { name: "Charlie" });
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
92
|
+
|
|
93
|
+
assertEquals(subscriber1Count, 1);
|
|
94
|
+
assertEquals(subscriber2Count, 1);
|
|
95
|
+
|
|
96
|
+
unsubscribe1();
|
|
97
|
+
unsubscribe2();
|
|
98
|
+
} finally {
|
|
99
|
+
await db.close();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
Deno.test("Subscriptions - Error Handling", async () => {
|
|
104
|
+
const db = new GunDB();
|
|
105
|
+
|
|
106
|
+
// Test subscription before ready
|
|
107
|
+
assertThrows(() => db.on("test", () => {}), Error, "Database not ready");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
Deno.test("Subscriptions - Off Method", async () => {
|
|
111
|
+
const db = new GunDB();
|
|
112
|
+
try {
|
|
113
|
+
const kvPath = await Deno.makeTempFile({
|
|
114
|
+
prefix: "kv_",
|
|
115
|
+
suffix: ".sqlite",
|
|
116
|
+
});
|
|
117
|
+
await db.ready(kvPath);
|
|
118
|
+
|
|
119
|
+
const id = "user:off";
|
|
120
|
+
let called = false;
|
|
121
|
+
|
|
122
|
+
const callback = () => {
|
|
123
|
+
called = true;
|
|
124
|
+
};
|
|
125
|
+
db.on(id, callback);
|
|
126
|
+
db.off(id, callback);
|
|
127
|
+
|
|
128
|
+
await db.put(id, { name: "Dave" });
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
130
|
+
|
|
131
|
+
assertEquals(called, false);
|
|
132
|
+
} finally {
|
|
133
|
+
await db.close();
|
|
134
|
+
}
|
|
135
|
+
});
|