holosphere 1.1.20 → 2.0.0-alpha1
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/.env.example +36 -0
- package/.eslintrc.json +16 -0
- package/.prettierrc.json +7 -0
- package/LICENSE +162 -38
- package/README.md +483 -367
- package/bin/holosphere-activitypub.js +158 -0
- package/cleanup-test-data.js +204 -0
- package/examples/demo.html +1333 -0
- package/examples/example-bot.js +197 -0
- package/package.json +47 -87
- package/scripts/check-bundle-size.js +54 -0
- package/scripts/check-quest-ids.js +77 -0
- package/scripts/import-holons.js +578 -0
- package/scripts/publish-to-relay.js +101 -0
- package/scripts/read-example.js +186 -0
- package/scripts/relay-diagnostic.js +59 -0
- package/scripts/relay-example.js +179 -0
- package/scripts/resync-to-relay.js +245 -0
- package/scripts/revert-import.js +196 -0
- package/scripts/test-hybrid-mode.js +108 -0
- package/scripts/test-local-storage.js +63 -0
- package/scripts/test-nostr-direct.js +55 -0
- package/scripts/test-read-data.js +45 -0
- package/scripts/test-write-read.js +63 -0
- package/scripts/verify-import.js +95 -0
- package/scripts/verify-relay-data.js +139 -0
- package/src/ai/aggregation.js +319 -0
- package/src/ai/breakdown.js +511 -0
- package/src/ai/classifier.js +217 -0
- package/src/ai/council.js +228 -0
- package/src/ai/embeddings.js +279 -0
- package/src/ai/federation-ai.js +324 -0
- package/src/ai/h3-ai.js +955 -0
- package/src/ai/index.js +112 -0
- package/src/ai/json-ops.js +225 -0
- package/src/ai/llm-service.js +205 -0
- package/src/ai/nl-query.js +223 -0
- package/src/ai/relationships.js +353 -0
- package/src/ai/schema-extractor.js +218 -0
- package/src/ai/spatial.js +293 -0
- package/src/ai/tts.js +194 -0
- package/src/content/social-protocols.js +168 -0
- package/src/core/holosphere.js +273 -0
- package/src/crypto/secp256k1.js +259 -0
- package/src/federation/discovery.js +334 -0
- package/src/federation/hologram.js +1042 -0
- package/src/federation/registry.js +386 -0
- package/src/hierarchical/upcast.js +110 -0
- package/src/index.js +2669 -0
- package/src/schema/validator.js +91 -0
- package/src/spatial/h3-operations.js +110 -0
- package/src/storage/backend-factory.js +125 -0
- package/src/storage/backend-interface.js +142 -0
- package/src/storage/backends/activitypub/server.js +653 -0
- package/src/storage/backends/activitypub-backend.js +272 -0
- package/src/storage/backends/gundb-backend.js +233 -0
- package/src/storage/backends/nostr-backend.js +136 -0
- package/src/storage/filesystem-storage-browser.js +41 -0
- package/src/storage/filesystem-storage.js +138 -0
- package/src/storage/global-tables.js +81 -0
- package/src/storage/gun-async.js +281 -0
- package/src/storage/gun-wrapper.js +221 -0
- package/src/storage/indexeddb-storage.js +122 -0
- package/src/storage/key-storage-simple.js +76 -0
- package/src/storage/key-storage.js +136 -0
- package/src/storage/memory-storage.js +59 -0
- package/src/storage/migration.js +338 -0
- package/src/storage/nostr-async.js +811 -0
- package/src/storage/nostr-client.js +939 -0
- package/src/storage/nostr-wrapper.js +211 -0
- package/src/storage/outbox-queue.js +208 -0
- package/src/storage/persistent-storage.js +109 -0
- package/src/storage/sync-service.js +164 -0
- package/src/subscriptions/manager.js +142 -0
- package/test-ai-real-api.js +202 -0
- package/tests/unit/ai/aggregation.test.js +295 -0
- package/tests/unit/ai/breakdown.test.js +446 -0
- package/tests/unit/ai/classifier.test.js +294 -0
- package/tests/unit/ai/council.test.js +262 -0
- package/tests/unit/ai/embeddings.test.js +384 -0
- package/tests/unit/ai/federation-ai.test.js +344 -0
- package/tests/unit/ai/h3-ai.test.js +458 -0
- package/tests/unit/ai/index.test.js +304 -0
- package/tests/unit/ai/json-ops.test.js +307 -0
- package/tests/unit/ai/llm-service.test.js +390 -0
- package/tests/unit/ai/nl-query.test.js +383 -0
- package/tests/unit/ai/relationships.test.js +311 -0
- package/tests/unit/ai/schema-extractor.test.js +384 -0
- package/tests/unit/ai/spatial.test.js +279 -0
- package/tests/unit/ai/tts.test.js +279 -0
- package/tests/unit/content.test.js +332 -0
- package/tests/unit/contract/core.test.js +88 -0
- package/tests/unit/contract/crypto.test.js +198 -0
- package/tests/unit/contract/data.test.js +223 -0
- package/tests/unit/contract/federation.test.js +181 -0
- package/tests/unit/contract/hierarchical.test.js +113 -0
- package/tests/unit/contract/schema.test.js +114 -0
- package/tests/unit/contract/social.test.js +217 -0
- package/tests/unit/contract/spatial.test.js +110 -0
- package/tests/unit/contract/subscriptions.test.js +128 -0
- package/tests/unit/contract/utils.test.js +159 -0
- package/tests/unit/core.test.js +152 -0
- package/tests/unit/crypto.test.js +328 -0
- package/tests/unit/federation.test.js +234 -0
- package/tests/unit/gun-async.test.js +252 -0
- package/tests/unit/hierarchical.test.js +399 -0
- package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
- package/tests/unit/integration/scenario-02-federation.test.js +76 -0
- package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
- package/tests/unit/integration/scenario-04-validation.test.js +129 -0
- package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
- package/tests/unit/integration/scenario-06-social.test.js +135 -0
- package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
- package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
- package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
- package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
- package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
- package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
- package/tests/unit/performance/benchmark.test.js +85 -0
- package/tests/unit/schema.test.js +213 -0
- package/tests/unit/spatial.test.js +158 -0
- package/tests/unit/storage.test.js +195 -0
- package/tests/unit/subscriptions.test.js +328 -0
- package/tests/unit/test-data-permanence-debug.js +197 -0
- package/tests/unit/test-data-permanence.js +340 -0
- package/tests/unit/test-key-persistence-fixed.js +148 -0
- package/tests/unit/test-key-persistence.js +172 -0
- package/tests/unit/test-relay-permanence.js +376 -0
- package/tests/unit/test-second-node.js +95 -0
- package/tests/unit/test-simple-write.js +89 -0
- package/vite.config.js +49 -0
- package/vitest.config.js +20 -0
- package/FEDERATION.md +0 -213
- package/compute.js +0 -298
- package/content.js +0 -980
- package/federation.js +0 -1234
- package/global.js +0 -736
- package/hexlib.js +0 -335
- package/hologram.js +0 -183
- package/holosphere-bundle.esm.js +0 -33256
- package/holosphere-bundle.js +0 -33287
- package/holosphere-bundle.min.js +0 -39
- package/holosphere.d.ts +0 -601
- package/holosphere.js +0 -719
- package/node.js +0 -246
- package/schema.js +0 -139
- package/utils.js +0 -302
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Benchmarks
|
|
3
|
+
* Tests initialization, write, read, subscription overhead, hologram resolution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
7
|
+
import HoloSphere from '../../../src/index.js';
|
|
8
|
+
|
|
9
|
+
describe('Performance Benchmarks', () => {
|
|
10
|
+
let hs;
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
hs = new HoloSphere({ relays: [], appName: 'perf-test', logLevel: 'ERROR' });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('initialization should complete in <50ms (without crypto)', () => {
|
|
17
|
+
const start = performance.now();
|
|
18
|
+
const instance = new HoloSphere({ relays: [], appName: 'test', logLevel: 'ERROR' });
|
|
19
|
+
const duration = performance.now() - start;
|
|
20
|
+
|
|
21
|
+
expect(instance).toBeDefined();
|
|
22
|
+
expect(duration).toBeLessThan(50);
|
|
23
|
+
console.log(`✓ Initialization: ${duration.toFixed(2)}ms`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('write operation should complete in <100ms (avg)', async () => {
|
|
27
|
+
const holonId = await hs.toHolon(37.7749, -122.4194, 9);
|
|
28
|
+
const iterations = 10;
|
|
29
|
+
const durations = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < iterations; i++) {
|
|
32
|
+
const start = performance.now();
|
|
33
|
+
await hs.write(holonId, 'perf', { id: `item-${i}`, value: i });
|
|
34
|
+
const duration = performance.now() - start;
|
|
35
|
+
durations.push(duration);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const avg = durations.reduce((a, b) => a + b, 0) / iterations;
|
|
39
|
+
expect(avg).toBeLessThan(100);
|
|
40
|
+
console.log(`✓ Write (avg): ${avg.toFixed(2)}ms`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('read operation should complete in <10ms (avg)', async () => {
|
|
44
|
+
const holonId = await hs.toHolon(37.7749, -122.4194, 9);
|
|
45
|
+
const iterations = 10;
|
|
46
|
+
const durations = [];
|
|
47
|
+
|
|
48
|
+
// Pre-write data
|
|
49
|
+
await hs.write(holonId, 'perf', { id: 'read-test', value: 42 });
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < iterations; i++) {
|
|
52
|
+
const start = performance.now();
|
|
53
|
+
await hs.read(holonId, 'perf', 'read-test');
|
|
54
|
+
const duration = performance.now() - start;
|
|
55
|
+
durations.push(duration);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const avg = durations.reduce((a, b) => a + b, 0) / iterations;
|
|
59
|
+
expect(avg).toBeLessThan(10);
|
|
60
|
+
console.log(`✓ Read (avg): ${avg.toFixed(2)}ms`);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('subscription overhead should be <5ms', () => {
|
|
64
|
+
const holonId = '8928342e20fffff'; // Pre-computed H3
|
|
65
|
+
const callback = () => {};
|
|
66
|
+
|
|
67
|
+
const start = performance.now();
|
|
68
|
+
const sub = hs.subscribe(holonId, 'perf', callback);
|
|
69
|
+
const duration = performance.now() - start;
|
|
70
|
+
|
|
71
|
+
expect(duration).toBeLessThan(5);
|
|
72
|
+
console.log(`✓ Subscription overhead: ${duration.toFixed(2)}ms`);
|
|
73
|
+
|
|
74
|
+
sub.unsubscribe();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('H3 conversion should complete in <5ms', async () => {
|
|
78
|
+
const start = performance.now();
|
|
79
|
+
await hs.toHolon(37.7749, -122.4194, 9);
|
|
80
|
+
const duration = performance.now() - start;
|
|
81
|
+
|
|
82
|
+
expect(duration).toBeLessThan(5);
|
|
83
|
+
console.log(`✓ H3 conversion: ${duration.toFixed(2)}ms`);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import HoloSphere from '../../src/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Unit: Schema Module', () => {
|
|
5
|
+
let hs;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
hs = new HoloSphere({ relays: [], appName: 'test-schema-unit' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('JSON Schema 2019 validation', () => {
|
|
12
|
+
it('should validate JSON Schema 2019 format', async () => {
|
|
13
|
+
const schema = {
|
|
14
|
+
$schema: 'https://json-schema.org/draft/2019-09/schema',
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
id: { type: 'string' },
|
|
18
|
+
value: { type: 'number' }
|
|
19
|
+
},
|
|
20
|
+
required: ['id', 'value']
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Schema should be valid
|
|
24
|
+
await expect(
|
|
25
|
+
hs.setSchema('test', 'test://schema/valid')
|
|
26
|
+
).resolves.toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should reject invalid schema format', async () => {
|
|
30
|
+
await expect(
|
|
31
|
+
hs.setSchema('test', { invalid: 'not a valid schema' })
|
|
32
|
+
).rejects.toThrow('ValidationError');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should support schema type validation', async () => {
|
|
36
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
37
|
+
await hs.setSchema('typed', {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
id: { type: 'string' },
|
|
41
|
+
value: { type: 'number' }
|
|
42
|
+
},
|
|
43
|
+
required: ['id', 'value']
|
|
44
|
+
}, true);
|
|
45
|
+
|
|
46
|
+
// Type validation should work in strict mode
|
|
47
|
+
await expect(
|
|
48
|
+
hs.write(holon, 'typed', {
|
|
49
|
+
id: 'test',
|
|
50
|
+
value: 'not-a-number' // Should fail if schema requires number
|
|
51
|
+
}, { strict: true })
|
|
52
|
+
).rejects.toThrow('ValidationError');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('Ajv validator compilation', () => {
|
|
57
|
+
it('should compile schema using Ajv', async () => {
|
|
58
|
+
await hs.setSchema('test', 'test://schema/compile');
|
|
59
|
+
|
|
60
|
+
// Schema should be compiled and ready for validation
|
|
61
|
+
const schema = await hs.getSchema('test');
|
|
62
|
+
expect(schema).not.toBe(null);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should reuse compiled validator for same schema', async () => {
|
|
66
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
67
|
+
await hs.setSchema('cached', 'test://schema/cache');
|
|
68
|
+
|
|
69
|
+
// Multiple validations should use cached compiled schema
|
|
70
|
+
await hs.write(holon, 'cached', { id: 'item-1', value: 1 });
|
|
71
|
+
await hs.write(holon, 'cached', { id: 'item-2', value: 2 });
|
|
72
|
+
await hs.write(holon, 'cached', { id: 'item-3', value: 3 });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('Schema caching (1-hour TTL)', () => {
|
|
77
|
+
it('should cache schema after first fetch', async () => {
|
|
78
|
+
await hs.setSchema('cache-test', 'test://schema/ttl');
|
|
79
|
+
|
|
80
|
+
const schema1 = await hs.getSchema('cache-test');
|
|
81
|
+
const schema2 = await hs.getSchema('cache-test');
|
|
82
|
+
|
|
83
|
+
// Should return same cached schema
|
|
84
|
+
expect(schema1).toEqual(schema2);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should use cached schema for validation', async () => {
|
|
88
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
89
|
+
await hs.setSchema('cached', 'test://schema/cached');
|
|
90
|
+
|
|
91
|
+
// First write compiles schema
|
|
92
|
+
await hs.write(holon, 'cached', { id: 'item-1', value: 1 });
|
|
93
|
+
|
|
94
|
+
// Subsequent writes use cached compiled schema
|
|
95
|
+
await hs.write(holon, 'cached', { id: 'item-2', value: 2 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should respect 1-hour cache TTL', async () => {
|
|
99
|
+
// Note: Actual TTL testing would require mocking time
|
|
100
|
+
await hs.setSchema('ttl-test', 'test://schema/ttl');
|
|
101
|
+
|
|
102
|
+
const schema = await hs.getSchema('ttl-test');
|
|
103
|
+
expect(schema).not.toBe(null);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('Cache expiration and recompilation', () => {
|
|
108
|
+
it('should allow cache invalidation via clearSchema', async () => {
|
|
109
|
+
await hs.setSchema('clear-test', 'test://schema/clear');
|
|
110
|
+
|
|
111
|
+
const before = await hs.getSchema('clear-test');
|
|
112
|
+
expect(before).not.toBe(null);
|
|
113
|
+
|
|
114
|
+
await hs.clearSchema('clear-test');
|
|
115
|
+
|
|
116
|
+
const after = await hs.getSchema('clear-test');
|
|
117
|
+
expect(after).toBe(null);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should recompile after cache expiration', async () => {
|
|
121
|
+
await hs.setSchema('recompile', 'test://schema/recompile');
|
|
122
|
+
|
|
123
|
+
// After clearing, should recompile on next use
|
|
124
|
+
await hs.clearSchema('recompile');
|
|
125
|
+
await hs.setSchema('recompile', 'test://schema/recompile');
|
|
126
|
+
|
|
127
|
+
const schema = await hs.getSchema('recompile');
|
|
128
|
+
expect(schema).not.toBe(null);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Strict vs permissive mode', () => {
|
|
133
|
+
it('should allow invalid data in permissive mode', async () => {
|
|
134
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
135
|
+
await hs.setSchema('permissive', 'test://schema/permissive', false);
|
|
136
|
+
|
|
137
|
+
// Invalid data should succeed with warning (not thrown)
|
|
138
|
+
const result = await hs.write(holon, 'permissive', {
|
|
139
|
+
id: 'invalid',
|
|
140
|
+
value: 'not-a-number'
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should reject invalid data in strict mode', async () => {
|
|
147
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
148
|
+
await hs.setSchema('strict', {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
id: { type: 'string' },
|
|
152
|
+
value: { type: 'number' }
|
|
153
|
+
},
|
|
154
|
+
required: ['id', 'value']
|
|
155
|
+
}, true);
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
hs.write(holon, 'strict', {
|
|
159
|
+
id: 'invalid',
|
|
160
|
+
value: 'not-a-number'
|
|
161
|
+
}, { strict: true })
|
|
162
|
+
).rejects.toThrow('ValidationError');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should log warnings in permissive mode', async () => {
|
|
166
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
167
|
+
await hs.setSchema('log-test', 'test://schema/log', false);
|
|
168
|
+
|
|
169
|
+
// Should log warning but not throw
|
|
170
|
+
const result = await hs.write(holon, 'log-test', {
|
|
171
|
+
id: 'invalid',
|
|
172
|
+
invalid: 'field'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should include Ajv errors in ValidationError', async () => {
|
|
179
|
+
const holon = await hs.toHolon(37.7749, -122.4194, 9);
|
|
180
|
+
await hs.setSchema('error-test', 'test://schema/error', true);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await hs.write(holon, 'error-test', {
|
|
184
|
+
id: 'bad',
|
|
185
|
+
value: 'invalid'
|
|
186
|
+
}, { strict: true });
|
|
187
|
+
} catch (err) {
|
|
188
|
+
expect(err.constructor.name).toBe('ValidationError');
|
|
189
|
+
expect(err).toHaveProperty('errors');
|
|
190
|
+
expect(Array.isArray(err.errors)).toBe(true);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Schema URI handling', () => {
|
|
196
|
+
it('should fetch schema from URI', async () => {
|
|
197
|
+
await hs.setSchema('uri-test', 'https://example.com/schema.json');
|
|
198
|
+
// Should attempt to fetch schema
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle schema fetch errors', async () => {
|
|
202
|
+
// Test that invalid schema objects are rejected
|
|
203
|
+
await expect(
|
|
204
|
+
hs.setSchema('fail-test', null)
|
|
205
|
+
).rejects.toThrow(Error);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should support local schema URIs', async () => {
|
|
209
|
+
await hs.setSchema('local-test', 'file://./schemas/local.json');
|
|
210
|
+
// Should handle local file URIs
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import HoloSphere from '../../src/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Unit: Spatial Module', () => {
|
|
5
|
+
const hs = new HoloSphere({ relays: [], appName: 'test-spatial-unit' });
|
|
6
|
+
|
|
7
|
+
describe('H3 validation', () => {
|
|
8
|
+
it('should validate H3 format starts with 8', () => {
|
|
9
|
+
expect(hs.isValidH3('8928342e20fffff')).toBe(true);
|
|
10
|
+
expect(hs.isValidH3('7928342e20fffff')).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should validate H3 format has at least 15 characters', () => {
|
|
14
|
+
expect(hs.isValidH3('8928342e20fffff')).toBe(true);
|
|
15
|
+
expect(hs.isValidH3('8928342e20f')).toBe(false);
|
|
16
|
+
expect(hs.isValidH3('8928342')).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should validate H3 contains only hex characters', () => {
|
|
20
|
+
expect(hs.isValidH3('8928342e20fffff')).toBe(true);
|
|
21
|
+
expect(hs.isValidH3('8928342g20fffff')).toBe(false);
|
|
22
|
+
expect(hs.isValidH3('8928342!20fffff')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should reject non-string inputs', () => {
|
|
26
|
+
expect(hs.isValidH3(123)).toBe(false);
|
|
27
|
+
expect(hs.isValidH3(null)).toBe(false);
|
|
28
|
+
expect(hs.isValidH3(undefined)).toBe(false);
|
|
29
|
+
expect(hs.isValidH3({})).toBe(false);
|
|
30
|
+
expect(hs.isValidH3([])).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('Coordinate to H3 conversion', () => {
|
|
35
|
+
it('should convert valid coordinates to H3', async () => {
|
|
36
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 9);
|
|
37
|
+
expect(typeof h3).toBe('string');
|
|
38
|
+
expect(hs.isValidH3(h3)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should produce consistent results for same coordinates', async () => {
|
|
42
|
+
const h3a = await hs.toHolon(37.7749, -122.4194, 9);
|
|
43
|
+
const h3b = await hs.toHolon(37.7749, -122.4194, 9);
|
|
44
|
+
expect(h3a).toBe(h3b);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should produce different H3 for different resolutions', async () => {
|
|
48
|
+
const h3res7 = await hs.toHolon(37.7749, -122.4194, 7);
|
|
49
|
+
const h3res9 = await hs.toHolon(37.7749, -122.4194, 9);
|
|
50
|
+
expect(h3res7).not.toBe(h3res9);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should handle resolution 0 (largest)', async () => {
|
|
54
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 0);
|
|
55
|
+
expect(hs.isValidH3(h3)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle resolution 15 (smallest)', async () => {
|
|
59
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 15);
|
|
60
|
+
expect(hs.isValidH3(h3)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should throw RangeError for invalid latitude', async () => {
|
|
64
|
+
await expect(hs.toHolon(91, -122.4194, 9)).rejects.toThrow(RangeError);
|
|
65
|
+
await expect(hs.toHolon(-91, -122.4194, 9)).rejects.toThrow(RangeError);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should throw RangeError for invalid longitude', async () => {
|
|
69
|
+
await expect(hs.toHolon(37.7749, 181, 9)).rejects.toThrow(RangeError);
|
|
70
|
+
await expect(hs.toHolon(37.7749, -181, 9)).rejects.toThrow(RangeError);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should throw RangeError for invalid resolution', async () => {
|
|
74
|
+
await expect(hs.toHolon(37.7749, -122.4194, -1)).rejects.toThrow(RangeError);
|
|
75
|
+
await expect(hs.toHolon(37.7749, -122.4194, 16)).rejects.toThrow(RangeError);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('Parent/child hierarchy navigation', () => {
|
|
80
|
+
it('should get parents for valid H3', async () => {
|
|
81
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 9);
|
|
82
|
+
const parents = await hs.getParents(h3);
|
|
83
|
+
expect(Array.isArray(parents)).toBe(true);
|
|
84
|
+
expect(parents.length).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should get parents up to specified maxResolution', async () => {
|
|
88
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 9);
|
|
89
|
+
const parents = await hs.getParents(h3, 5);
|
|
90
|
+
expect(Array.isArray(parents)).toBe(true);
|
|
91
|
+
// Should stop at resolution 5
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return empty array for resolution 0 (no parents)', async () => {
|
|
95
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 0);
|
|
96
|
+
const parents = await hs.getParents(h3);
|
|
97
|
+
expect(parents).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should get 7 children for valid H3', async () => {
|
|
101
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 9);
|
|
102
|
+
const children = await hs.getChildren(h3);
|
|
103
|
+
expect(Array.isArray(children)).toBe(true);
|
|
104
|
+
expect(children.length).toBe(7);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should throw Error for resolution 15 (no children)', async () => {
|
|
108
|
+
const h3 = await hs.toHolon(37.7749, -122.4194, 15);
|
|
109
|
+
await expect(hs.getChildren(h3)).rejects.toThrow(Error);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should validate parent-child relationships', async () => {
|
|
113
|
+
const parent = await hs.toHolon(37.7749, -122.4194, 7);
|
|
114
|
+
const children = await hs.getChildren(parent);
|
|
115
|
+
|
|
116
|
+
// One of the children should contain our test point at resolution 8
|
|
117
|
+
expect(children.length).toBe(7);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Resolution extraction', () => {
|
|
122
|
+
it('should extract resolution from H3 ID', async () => {
|
|
123
|
+
const h3res5 = await hs.toHolon(37.7749, -122.4194, 5);
|
|
124
|
+
const h3res9 = await hs.toHolon(37.7749, -122.4194, 9);
|
|
125
|
+
const h3res12 = await hs.toHolon(37.7749, -122.4194, 12);
|
|
126
|
+
|
|
127
|
+
// getResolution should be available if implemented
|
|
128
|
+
if (typeof hs.getResolution === 'function') {
|
|
129
|
+
expect(hs.getResolution(h3res5)).toBe(5);
|
|
130
|
+
expect(hs.getResolution(h3res9)).toBe(9);
|
|
131
|
+
expect(hs.getResolution(h3res12)).toBe(12);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Edge cases', () => {
|
|
137
|
+
it('should handle coordinates at poles', async () => {
|
|
138
|
+
const northPole = await hs.toHolon(89.9, 0, 5);
|
|
139
|
+
const southPole = await hs.toHolon(-89.9, 0, 5);
|
|
140
|
+
|
|
141
|
+
expect(hs.isValidH3(northPole)).toBe(true);
|
|
142
|
+
expect(hs.isValidH3(southPole)).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle coordinates at dateline', async () => {
|
|
146
|
+
const east = await hs.toHolon(0, 179.9, 5);
|
|
147
|
+
const west = await hs.toHolon(0, -179.9, 5);
|
|
148
|
+
|
|
149
|
+
expect(hs.isValidH3(east)).toBe(true);
|
|
150
|
+
expect(hs.isValidH3(west)).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle coordinates at equator and prime meridian', async () => {
|
|
154
|
+
const origin = await hs.toHolon(0, 0, 9);
|
|
155
|
+
expect(hs.isValidH3(origin)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import HoloSphere from '../../src/index.js';
|
|
3
|
+
|
|
4
|
+
describe('Unit: Storage Module', () => {
|
|
5
|
+
let hs;
|
|
6
|
+
let testHolonId;
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
hs = new HoloSphere({ relays: [], appName: 'test-storage-unit' });
|
|
10
|
+
testHolonId = await hs.toHolon(37.7749, -122.4194, 9);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('Path construction', () => {
|
|
14
|
+
it('should construct path with appname/holon/lens/key format', async () => {
|
|
15
|
+
// Internal path construction (implementation detail)
|
|
16
|
+
// Path should follow format: appname/holon/lens/key
|
|
17
|
+
await hs.write(testHolonId, 'test-lens', { id: 'test-key', data: 'value' });
|
|
18
|
+
|
|
19
|
+
const result = await hs.read(testHolonId, 'test-lens', 'test-key');
|
|
20
|
+
expect(result).not.toBe(null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should encode special characters in paths', async () => {
|
|
24
|
+
const lensWithSpaces = 'my test lens';
|
|
25
|
+
const keyWithSpecial = 'key!@#$%';
|
|
26
|
+
|
|
27
|
+
await hs.write(testHolonId, lensWithSpaces, {
|
|
28
|
+
id: keyWithSpecial,
|
|
29
|
+
data: 'test'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await hs.read(testHolonId, lensWithSpaces, keyWithSpecial);
|
|
33
|
+
expect(result).not.toBe(null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle unicode characters in paths', async () => {
|
|
37
|
+
const unicodeLens = '测试镜头';
|
|
38
|
+
const unicodeKey = 'キー001';
|
|
39
|
+
|
|
40
|
+
await hs.write(testHolonId, unicodeLens, {
|
|
41
|
+
id: unicodeKey,
|
|
42
|
+
data: 'unicode test'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = await hs.read(testHolonId, unicodeLens, unicodeKey);
|
|
46
|
+
expect(result).not.toBe(null);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Gun graph operations', () => {
|
|
51
|
+
it('should perform Gun .put() for write', async () => {
|
|
52
|
+
const success = await hs.write(testHolonId, 'test', {
|
|
53
|
+
id: 'item-1',
|
|
54
|
+
value: 'data'
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(success).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should perform Gun .once() for read', async () => {
|
|
61
|
+
await hs.write(testHolonId, 'test', { id: 'item-1', value: 'data' });
|
|
62
|
+
|
|
63
|
+
const result = await hs.read(testHolonId, 'test', 'item-1');
|
|
64
|
+
expect(result).toMatchObject({ id: 'item-1', value: 'data' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return null for non-existent data', async () => {
|
|
68
|
+
const result = await hs.read(testHolonId, 'nonexistent', 'nonexistent');
|
|
69
|
+
expect(result).toBe(null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should perform Gun .put(null) for delete (tombstone)', async () => {
|
|
73
|
+
await hs.write(testHolonId, 'test', { id: 'item-1', value: 'data' });
|
|
74
|
+
|
|
75
|
+
const deleted = await hs.delete(testHolonId, 'test', 'item-1');
|
|
76
|
+
expect(deleted).toBe(true);
|
|
77
|
+
|
|
78
|
+
const result = await hs.read(testHolonId, 'test', 'item-1');
|
|
79
|
+
expect(result).toBe(null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should perform Gun .on() for subscribe', () => {
|
|
83
|
+
const callback = () => {};
|
|
84
|
+
const subscription = hs.subscribe(testHolonId, 'test', callback);
|
|
85
|
+
|
|
86
|
+
expect(subscription).toHaveProperty('unsubscribe');
|
|
87
|
+
subscription.unsubscribe();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Radisk persistence', () => {
|
|
92
|
+
it('should persist data with radisk enabled', async () => {
|
|
93
|
+
const hsWithRadisk = new HoloSphere({
|
|
94
|
+
appName: 'radisk-test',
|
|
95
|
+
radisk: true
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const holon = await hsWithRadisk.toHolon(37.7749, -122.4194, 9);
|
|
99
|
+
await hsWithRadisk.write(holon, 'persist', {
|
|
100
|
+
id: 'persist-1',
|
|
101
|
+
value: 'persisted'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Data should be written to disk via radisk
|
|
105
|
+
const result = await hsWithRadisk.read(holon, 'persist', 'persist-1');
|
|
106
|
+
expect(result).not.toBe(null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should work without radisk when disabled', async () => {
|
|
110
|
+
const hsNoRadisk = new HoloSphere({
|
|
111
|
+
appName: 'no-radisk-test',
|
|
112
|
+
radisk: false
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const holon = await hsNoRadisk.toHolon(37.7749, -122.4194, 9);
|
|
116
|
+
await hsNoRadisk.write(holon, 'memory', {
|
|
117
|
+
id: 'mem-1',
|
|
118
|
+
value: 'in-memory'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await hsNoRadisk.read(holon, 'memory', 'mem-1');
|
|
122
|
+
expect(result).not.toBe(null);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Path encoding', () => {
|
|
127
|
+
it('should encode forward slashes in lens names', async () => {
|
|
128
|
+
const lensWithSlash = 'category/subcategory';
|
|
129
|
+
await hs.write(testHolonId, lensWithSlash, {
|
|
130
|
+
id: 'item-1',
|
|
131
|
+
data: 'test'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await hs.read(testHolonId, lensWithSlash, 'item-1');
|
|
135
|
+
expect(result).not.toBe(null);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should encode dots in keys', async () => {
|
|
139
|
+
await hs.write(testHolonId, 'test', {
|
|
140
|
+
id: 'item.with.dots',
|
|
141
|
+
data: 'test'
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await hs.read(testHolonId, 'test', 'item.with.dots');
|
|
145
|
+
expect(result).not.toBe(null);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle empty strings gracefully', async () => {
|
|
149
|
+
await expect(
|
|
150
|
+
hs.write(testHolonId, '', { id: 'test' })
|
|
151
|
+
).rejects.toThrow(Error);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('Error handling', () => {
|
|
156
|
+
it('should handle Gun connection errors', async () => {
|
|
157
|
+
// Test with invalid configuration
|
|
158
|
+
const hsInvalid = new HoloSphere({
|
|
159
|
+
appName: 'invalid-test',
|
|
160
|
+
peers: ['https://nonexistent.invalid/gun']
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Should still work locally with radisk
|
|
164
|
+
const holon = await hsInvalid.toHolon(37.7749, -122.4194, 9);
|
|
165
|
+
const success = await hsInvalid.write(holon, 'test', {
|
|
166
|
+
id: 'test-1',
|
|
167
|
+
data: 'local'
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// May succeed locally even if peer connection fails
|
|
171
|
+
expect(typeof success).toBe('boolean');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle malformed data gracefully', async () => {
|
|
175
|
+
// Gun should handle various data types
|
|
176
|
+
await hs.write(testHolonId, 'test', {
|
|
177
|
+
id: 'complex',
|
|
178
|
+
nested: { deep: { object: 'value' } },
|
|
179
|
+
array: [1, 2, 3],
|
|
180
|
+
number: 42,
|
|
181
|
+
boolean: true
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const result = await hs.read(testHolonId, 'test', 'complex');
|
|
185
|
+
expect(result).not.toBe(null);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should throw ValidationError on invalid input', async () => {
|
|
189
|
+
// Should throw ValidationError with empty holon ID
|
|
190
|
+
await expect(
|
|
191
|
+
hs.write('', 'test', { id: 'test' })
|
|
192
|
+
).rejects.toThrow('holonId must be a non-empty string');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|