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.
Files changed (84) hide show
  1. package/LICENSE +72 -0
  2. package/README.md +322 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +253 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/node-index.d.ts +52 -0
  9. package/dist/node-index.d.ts.map +1 -0
  10. package/dist/node-index.js +359 -0
  11. package/dist/node-index.js.map +1 -0
  12. package/dist/node-wrapper.d.ts +44 -0
  13. package/dist/node-wrapper.d.ts.map +1 -0
  14. package/dist/node-wrapper.js +294 -0
  15. package/dist/node-wrapper.js.map +1 -0
  16. package/dist/types/index.d.ts +28 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/index.js +3 -0
  19. package/dist/types/index.js.map +1 -0
  20. package/dist/types/node-types.d.ts +59 -0
  21. package/dist/types/node-types.d.ts.map +1 -0
  22. package/dist/types/node-types.js +6 -0
  23. package/dist/types/node-types.js.map +1 -0
  24. package/dist/vscode/extension.d.ts +81 -0
  25. package/dist/vscode/extension.d.ts.map +1 -0
  26. package/dist/vscode/extension.js +309 -0
  27. package/dist/vscode/extension.js.map +1 -0
  28. package/examples/basic-usage.d.ts +2 -0
  29. package/examples/basic-usage.d.ts.map +1 -0
  30. package/examples/basic-usage.js +26 -0
  31. package/examples/basic-usage.js.map +1 -0
  32. package/examples/basic-usage.ts +29 -0
  33. package/examples/vscode-extension-example/README.md +95 -0
  34. package/examples/vscode-extension-example/package.json +49 -0
  35. package/examples/vscode-extension-example/src/extension.ts +163 -0
  36. package/examples/vscode-extension-example/tsconfig.json +12 -0
  37. package/examples/vscode-extension-integration.d.ts +24 -0
  38. package/examples/vscode-extension-integration.d.ts.map +1 -0
  39. package/examples/vscode-extension-integration.js +285 -0
  40. package/examples/vscode-extension-integration.js.map +1 -0
  41. package/examples/vscode-extension-integration.ts +41 -0
  42. package/package.json +115 -0
  43. package/scripts/compiled-crud-verify.ts +28 -0
  44. package/scripts/dogfood.ts +258 -0
  45. package/scripts/postinstall.js +155 -0
  46. package/scripts/run-tests.ts +175 -0
  47. package/scripts/setup-libclang.ps1 +209 -0
  48. package/src/benchmarks/memory-benchmarks.ts +316 -0
  49. package/src/benchmarks/run-benchmarks.ts +293 -0
  50. package/src/cli.ts +231 -0
  51. package/src/config.ts +49 -0
  52. package/src/core/crdt.ts +104 -0
  53. package/src/core/database.ts +494 -0
  54. package/src/healthcheck.ts +156 -0
  55. package/src/http/api-server.ts +334 -0
  56. package/src/index.ts +28 -0
  57. package/src/logic/rules.ts +44 -0
  58. package/src/main.rs +3 -0
  59. package/src/main.ts +190 -0
  60. package/src/network/websocket-server.ts +115 -0
  61. package/src/node-index.ts +385 -0
  62. package/src/node-wrapper.ts +320 -0
  63. package/src/sqlite-compat.ts +586 -0
  64. package/src/sqlite3-compat.ts +55 -0
  65. package/src/storage/kv-storage.ts +71 -0
  66. package/src/tests/core.test.ts +281 -0
  67. package/src/tests/fixtures/performance-data.json +71 -0
  68. package/src/tests/fixtures/test-data.json +124 -0
  69. package/src/tests/integration/api-server.test.ts +232 -0
  70. package/src/tests/integration/mesh-network.test.ts +297 -0
  71. package/src/tests/logic.test.ts +30 -0
  72. package/src/tests/performance/load.test.ts +288 -0
  73. package/src/tests/security/input-validation.test.ts +282 -0
  74. package/src/tests/unit/core.test.ts +216 -0
  75. package/src/tests/unit/subscriptions.test.ts +135 -0
  76. package/src/tests/unit/vector-search.test.ts +173 -0
  77. package/src/tests/vscode_extension_test.ts +253 -0
  78. package/src/types/index.ts +32 -0
  79. package/src/types/node-types.ts +66 -0
  80. package/src/util/debug.ts +14 -0
  81. package/src/vector/index.ts +59 -0
  82. package/src/vscode/extension.ts +364 -0
  83. package/web/README.md +27 -0
  84. 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
+ });