holosphere 1.1.20 → 2.0.0-alpha0

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 (146) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/README.md +483 -367
  5. package/bin/holosphere-activitypub.js +158 -0
  6. package/cleanup-test-data.js +204 -0
  7. package/examples/demo.html +1333 -0
  8. package/examples/example-bot.js +197 -0
  9. package/package.json +47 -87
  10. package/scripts/check-bundle-size.js +54 -0
  11. package/scripts/check-quest-ids.js +77 -0
  12. package/scripts/import-holons.js +578 -0
  13. package/scripts/publish-to-relay.js +101 -0
  14. package/scripts/read-example.js +186 -0
  15. package/scripts/relay-diagnostic.js +59 -0
  16. package/scripts/relay-example.js +179 -0
  17. package/scripts/resync-to-relay.js +245 -0
  18. package/scripts/revert-import.js +196 -0
  19. package/scripts/test-hybrid-mode.js +108 -0
  20. package/scripts/test-local-storage.js +63 -0
  21. package/scripts/test-nostr-direct.js +55 -0
  22. package/scripts/test-read-data.js +45 -0
  23. package/scripts/test-write-read.js +63 -0
  24. package/scripts/verify-import.js +95 -0
  25. package/scripts/verify-relay-data.js +139 -0
  26. package/src/ai/aggregation.js +319 -0
  27. package/src/ai/breakdown.js +511 -0
  28. package/src/ai/classifier.js +217 -0
  29. package/src/ai/council.js +228 -0
  30. package/src/ai/embeddings.js +279 -0
  31. package/src/ai/federation-ai.js +324 -0
  32. package/src/ai/h3-ai.js +955 -0
  33. package/src/ai/index.js +112 -0
  34. package/src/ai/json-ops.js +225 -0
  35. package/src/ai/llm-service.js +205 -0
  36. package/src/ai/nl-query.js +223 -0
  37. package/src/ai/relationships.js +353 -0
  38. package/src/ai/schema-extractor.js +218 -0
  39. package/src/ai/spatial.js +293 -0
  40. package/src/ai/tts.js +194 -0
  41. package/src/content/social-protocols.js +168 -0
  42. package/src/core/holosphere.js +273 -0
  43. package/src/crypto/secp256k1.js +259 -0
  44. package/src/federation/discovery.js +334 -0
  45. package/src/federation/hologram.js +1042 -0
  46. package/src/federation/registry.js +386 -0
  47. package/src/hierarchical/upcast.js +110 -0
  48. package/src/index.js +2669 -0
  49. package/src/schema/validator.js +91 -0
  50. package/src/spatial/h3-operations.js +110 -0
  51. package/src/storage/backend-factory.js +125 -0
  52. package/src/storage/backend-interface.js +142 -0
  53. package/src/storage/backends/activitypub/server.js +653 -0
  54. package/src/storage/backends/activitypub-backend.js +272 -0
  55. package/src/storage/backends/gundb-backend.js +233 -0
  56. package/src/storage/backends/nostr-backend.js +136 -0
  57. package/src/storage/filesystem-storage-browser.js +41 -0
  58. package/src/storage/filesystem-storage.js +138 -0
  59. package/src/storage/global-tables.js +81 -0
  60. package/src/storage/gun-async.js +281 -0
  61. package/src/storage/gun-wrapper.js +221 -0
  62. package/src/storage/indexeddb-storage.js +122 -0
  63. package/src/storage/key-storage-simple.js +76 -0
  64. package/src/storage/key-storage.js +136 -0
  65. package/src/storage/memory-storage.js +59 -0
  66. package/src/storage/migration.js +338 -0
  67. package/src/storage/nostr-async.js +811 -0
  68. package/src/storage/nostr-client.js +939 -0
  69. package/src/storage/nostr-wrapper.js +211 -0
  70. package/src/storage/outbox-queue.js +208 -0
  71. package/src/storage/persistent-storage.js +109 -0
  72. package/src/storage/sync-service.js +164 -0
  73. package/src/subscriptions/manager.js +142 -0
  74. package/test-ai-real-api.js +202 -0
  75. package/tests/unit/ai/aggregation.test.js +295 -0
  76. package/tests/unit/ai/breakdown.test.js +446 -0
  77. package/tests/unit/ai/classifier.test.js +294 -0
  78. package/tests/unit/ai/council.test.js +262 -0
  79. package/tests/unit/ai/embeddings.test.js +384 -0
  80. package/tests/unit/ai/federation-ai.test.js +344 -0
  81. package/tests/unit/ai/h3-ai.test.js +458 -0
  82. package/tests/unit/ai/index.test.js +304 -0
  83. package/tests/unit/ai/json-ops.test.js +307 -0
  84. package/tests/unit/ai/llm-service.test.js +390 -0
  85. package/tests/unit/ai/nl-query.test.js +383 -0
  86. package/tests/unit/ai/relationships.test.js +311 -0
  87. package/tests/unit/ai/schema-extractor.test.js +384 -0
  88. package/tests/unit/ai/spatial.test.js +279 -0
  89. package/tests/unit/ai/tts.test.js +279 -0
  90. package/tests/unit/content.test.js +332 -0
  91. package/tests/unit/contract/core.test.js +88 -0
  92. package/tests/unit/contract/crypto.test.js +198 -0
  93. package/tests/unit/contract/data.test.js +223 -0
  94. package/tests/unit/contract/federation.test.js +181 -0
  95. package/tests/unit/contract/hierarchical.test.js +113 -0
  96. package/tests/unit/contract/schema.test.js +114 -0
  97. package/tests/unit/contract/social.test.js +217 -0
  98. package/tests/unit/contract/spatial.test.js +110 -0
  99. package/tests/unit/contract/subscriptions.test.js +128 -0
  100. package/tests/unit/contract/utils.test.js +159 -0
  101. package/tests/unit/core.test.js +152 -0
  102. package/tests/unit/crypto.test.js +328 -0
  103. package/tests/unit/federation.test.js +234 -0
  104. package/tests/unit/gun-async.test.js +252 -0
  105. package/tests/unit/hierarchical.test.js +399 -0
  106. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  107. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  108. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  109. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  110. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  111. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  112. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  113. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  114. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  115. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  116. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  117. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  118. package/tests/unit/performance/benchmark.test.js +85 -0
  119. package/tests/unit/schema.test.js +213 -0
  120. package/tests/unit/spatial.test.js +158 -0
  121. package/tests/unit/storage.test.js +195 -0
  122. package/tests/unit/subscriptions.test.js +328 -0
  123. package/tests/unit/test-data-permanence-debug.js +197 -0
  124. package/tests/unit/test-data-permanence.js +340 -0
  125. package/tests/unit/test-key-persistence-fixed.js +148 -0
  126. package/tests/unit/test-key-persistence.js +172 -0
  127. package/tests/unit/test-relay-permanence.js +376 -0
  128. package/tests/unit/test-second-node.js +95 -0
  129. package/tests/unit/test-simple-write.js +89 -0
  130. package/vite.config.js +49 -0
  131. package/vitest.config.js +20 -0
  132. package/FEDERATION.md +0 -213
  133. package/compute.js +0 -298
  134. package/content.js +0 -980
  135. package/federation.js +0 -1234
  136. package/global.js +0 -736
  137. package/hexlib.js +0 -335
  138. package/hologram.js +0 -183
  139. package/holosphere-bundle.esm.js +0 -33256
  140. package/holosphere-bundle.js +0 -33287
  141. package/holosphere-bundle.min.js +0 -39
  142. package/holosphere.d.ts +0 -601
  143. package/holosphere.js +0 -719
  144. package/node.js +0 -246
  145. package/schema.js +0 -139
  146. package/utils.js +0 -302
@@ -0,0 +1,138 @@
1
+ /**
2
+ * File system storage adapter for Node.js
3
+ * Stores events as JSON files in a directory structure
4
+ * This module is only loaded in Node.js environments
5
+ */
6
+
7
+ // Guard against browser environments
8
+ if (typeof window !== 'undefined') {
9
+ throw new Error('FileSystemStorage is not available in browser environments');
10
+ }
11
+
12
+ import { PersistentStorage } from './persistent-storage.js';
13
+ import { promises as fs } from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+
17
+ export class FileSystemStorage extends PersistentStorage {
18
+ constructor(baseDir = null) {
19
+ super();
20
+ this.baseDir = baseDir || path.join(os.homedir(), '.holosphere');
21
+ this.namespace = null;
22
+ this.storageDir = null;
23
+ }
24
+
25
+ async init(namespace) {
26
+ this.namespace = namespace;
27
+ this.storageDir = path.join(this.baseDir, namespace);
28
+
29
+ // Create directory if it doesn't exist
30
+ try {
31
+ await fs.mkdir(this.storageDir, { recursive: true });
32
+ } catch (error) {
33
+ if (error.code !== 'EEXIST') {
34
+ throw error;
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Convert key to safe filename
41
+ * @private
42
+ */
43
+ _keyToFilename(key) {
44
+ // Replace unsafe characters
45
+ return key.replace(/[^a-zA-Z0-9-_]/g, '_') + '.json';
46
+ }
47
+
48
+ /**
49
+ * Get full file path for key
50
+ * @private
51
+ */
52
+ _getFilePath(key) {
53
+ return path.join(this.storageDir, this._keyToFilename(key));
54
+ }
55
+
56
+ async put(key, event) {
57
+ const filePath = this._getFilePath(key);
58
+ const data = JSON.stringify(event, null, 2);
59
+ await fs.writeFile(filePath, data, 'utf8');
60
+ }
61
+
62
+ async get(key) {
63
+ const filePath = this._getFilePath(key);
64
+ try {
65
+ const data = await fs.readFile(filePath, 'utf8');
66
+ return JSON.parse(data);
67
+ } catch (error) {
68
+ if (error.code === 'ENOENT') {
69
+ return null; // File doesn't exist
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ async getAll(prefix) {
76
+ const results = [];
77
+
78
+ try {
79
+ const files = await fs.readdir(this.storageDir);
80
+
81
+ for (const file of files) {
82
+ if (!file.endsWith('.json')) continue;
83
+
84
+ const filePath = path.join(this.storageDir, file);
85
+ try {
86
+ const data = await fs.readFile(filePath, 'utf8');
87
+ const event = JSON.parse(data);
88
+
89
+ // Check if this event matches the prefix
90
+ // The key is stored in the event's d-tag or we derive it from filename
91
+ const dTag = event.tags?.find(t => t[0] === 'd');
92
+ const key = dTag ? dTag[1] : file.replace('.json', '').replace(/_/g, '/');
93
+
94
+ if (key.startsWith(prefix)) {
95
+ results.push(event);
96
+ }
97
+ } catch (error) {
98
+ // Skip files that can't be read or parsed (silently)
99
+ // This is common for outbox queue entries and other non-event files
100
+ }
101
+ }
102
+ } catch (error) {
103
+ if (error.code !== 'ENOENT') {
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ return results;
109
+ }
110
+
111
+ async delete(key) {
112
+ const filePath = this._getFilePath(key);
113
+ try {
114
+ await fs.unlink(filePath);
115
+ } catch (error) {
116
+ if (error.code !== 'ENOENT') {
117
+ throw error;
118
+ }
119
+ }
120
+ }
121
+
122
+ async clear() {
123
+ try {
124
+ const files = await fs.readdir(this.storageDir);
125
+ await Promise.all(
126
+ files.map(file => fs.unlink(path.join(this.storageDir, file)))
127
+ );
128
+ } catch (error) {
129
+ if (error.code !== 'ENOENT') {
130
+ throw error;
131
+ }
132
+ }
133
+ }
134
+
135
+ async close() {
136
+ // Nothing to close for file system storage
137
+ }
138
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Global Data Operations
3
+ * Non-location-specific storage (FR-044, FR-045)
4
+ */
5
+
6
+ import { write, read, readAll, update, deleteData, deleteAll } from './nostr-wrapper.js';
7
+
8
+ /**
9
+ * Write data to global table
10
+ * @param {Object} client - Nostr client instance
11
+ * @param {string} appname - Application namespace
12
+ * @param {string} table - Global table name
13
+ * @param {Object} data - Data to write (must contain id field)
14
+ * @returns {Promise<boolean>} Success indicator
15
+ */
16
+ export async function writeGlobal(client, appname, table, data) {
17
+ // Auto-generate ID if not provided
18
+ if (!data.id) {
19
+ data.id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
20
+ }
21
+
22
+ const path = `${appname}/${table}/${data.id}`;
23
+ return write(client, path, data);
24
+ }
25
+
26
+ /**
27
+ * Read data from global table
28
+ * @param {Object} client - Nostr client instance
29
+ * @param {string} appname - Application namespace
30
+ * @param {string} table - Global table name
31
+ * @param {string} key - Data key (optional, if not provided returns all)
32
+ * @returns {Promise<Object|Object[]|null>} Data or null
33
+ */
34
+ export async function readGlobal(client, appname, table, key = null) {
35
+ if (key) {
36
+ const path = `${appname}/${table}/${key}`;
37
+ return read(client, path);
38
+ } else {
39
+ const path = `${appname}/${table}`;
40
+ return readAll(client, path);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Update data in global table
46
+ * @param {Object} client - Nostr client instance
47
+ * @param {string} appname - Application namespace
48
+ * @param {string} table - Global table name
49
+ * @param {string} key - Data key
50
+ * @param {Object} updates - Fields to update
51
+ * @returns {Promise<boolean>} Success indicator
52
+ */
53
+ export async function updateGlobal(client, appname, table, key, updates) {
54
+ const path = `${appname}/${table}/${key}`;
55
+ return update(client, path, updates);
56
+ }
57
+
58
+ /**
59
+ * Delete data from global table
60
+ * @param {Object} client - Nostr client instance
61
+ * @param {string} appname - Application namespace
62
+ * @param {string} table - Global table name
63
+ * @param {string} key - Data key
64
+ * @returns {Promise<boolean>} Success indicator
65
+ */
66
+ export async function deleteGlobal(client, appname, table, key) {
67
+ const path = `${appname}/${table}/${key}`;
68
+ return deleteData(client, path);
69
+ }
70
+
71
+ /**
72
+ * Delete all data from global table
73
+ * @param {Object} client - Nostr client instance
74
+ * @param {string} appname - Application namespace
75
+ * @param {string} table - Global table name
76
+ * @returns {Promise<Object>} Deletion results
77
+ */
78
+ export async function deleteAllGlobal(client, appname, table) {
79
+ const path = `${appname}/${table}`;
80
+ return deleteAll(client, path);
81
+ }
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Gun Async Utilities
3
+ * Provides Promise-based wrappers and async patterns for Gun operations
4
+ */
5
+
6
+ /**
7
+ * Get data from Gun using native .then() support
8
+ * @param {Object} gunChain - Gun chain reference
9
+ * @param {number} timeout - Timeout in ms (default 1000ms)
10
+ * @returns {Promise<any>} Promise resolving to data
11
+ */
12
+ export function gunPromise(gunChain, timeout = 1000) {
13
+ return new Promise((resolve, reject) => {
14
+ let settled = false;
15
+
16
+ const timer = setTimeout(() => {
17
+ if (!settled) {
18
+ settled = true;
19
+ resolve(null);
20
+ }
21
+ }, timeout);
22
+
23
+ gunChain.once((data) => {
24
+ if (!settled) {
25
+ settled = true;
26
+ clearTimeout(timer);
27
+ resolve(data || null);
28
+ }
29
+ });
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Wait for Gun write acknowledgement
35
+ * @param {Object} gunChain - Gun chain reference
36
+ * @param {any} data - Data to write
37
+ * @param {number} timeout - Timeout in ms (default 1000ms)
38
+ * @returns {Promise<Object>} Promise resolving to ack object
39
+ */
40
+ export function gunPut(gunChain, data, timeout = 1000) {
41
+ return new Promise((resolve, reject) => {
42
+ let settled = false;
43
+
44
+ const timer = setTimeout(() => {
45
+ if (!settled) {
46
+ settled = true;
47
+ resolve({ ok: true, timeout: true });
48
+ }
49
+ }, timeout);
50
+
51
+ gunChain.put(data, (ack) => {
52
+ if (!settled) {
53
+ settled = true;
54
+ clearTimeout(timer);
55
+ if (ack.err) {
56
+ reject(new Error(ack.err));
57
+ } else {
58
+ resolve(ack);
59
+ }
60
+ }
61
+ });
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Get all items from a Gun map
67
+ * @param {Object} gunChain - Gun chain reference
68
+ * @param {number} timeout - Timeout in ms (default 300ms)
69
+ * @returns {Promise<Object>} Promise resolving to map of items
70
+ */
71
+ export async function gunMap(gunChain, timeout = 300) {
72
+ return new Promise((resolve) => {
73
+ const items = {};
74
+
75
+ gunChain.map().once((data, key) => {
76
+ if (data && !key.startsWith('_')) {
77
+ items[key] = data;
78
+ }
79
+ });
80
+
81
+ setTimeout(() => {
82
+ resolve(items);
83
+ }, timeout);
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Load full chain data (follows all references)
89
+ * @param {Object} gunChain - Gun chain reference
90
+ * @param {number} depth - Max depth to traverse (default 3)
91
+ * @returns {Promise<any>} Promise resolving to loaded data
92
+ */
93
+ export async function gunLoad(gunChain, depth = 3) {
94
+ return new Promise((resolve) => {
95
+ let result = null;
96
+
97
+ gunChain.load((data) => {
98
+ result = data;
99
+ }, { wait: 100 });
100
+
101
+ setTimeout(() => {
102
+ resolve(result);
103
+ }, 100 * depth);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Async iterator for Gun map
109
+ * @param {Object} gunChain - Gun chain reference
110
+ * @returns {AsyncGenerator} Async generator yielding [key, value] pairs
111
+ */
112
+ export async function* gunMapIterator(gunChain) {
113
+ const items = await gunMap(gunChain);
114
+ for (const [key, value] of Object.entries(items)) {
115
+ yield [key, value];
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Collect Gun on() stream into array over time
121
+ * @param {Object} gunChain - Gun chain reference
122
+ * @param {number} duration - Collection duration in ms
123
+ * @returns {Promise<Array>} Promise resolving to array of data
124
+ */
125
+ export async function gunCollect(gunChain, duration = 500) {
126
+ return new Promise((resolve) => {
127
+ const results = [];
128
+ const seen = new Set();
129
+
130
+ const listener = gunChain.on((data, key) => {
131
+ if (data && !seen.has(key)) {
132
+ seen.add(key);
133
+ results.push({ key, data });
134
+ }
135
+ });
136
+
137
+ setTimeout(() => {
138
+ listener.off();
139
+ resolve(results);
140
+ }, duration);
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Wait for specific condition on Gun data
146
+ * @param {Object} gunChain - Gun chain reference
147
+ * @param {Function} predicate - Condition function (data) => boolean
148
+ * @param {number} timeout - Timeout in ms (default 5000ms)
149
+ * @returns {Promise<any>} Promise resolving when condition is met
150
+ */
151
+ export async function gunWaitFor(gunChain, predicate, timeout = 5000) {
152
+ return new Promise((resolve, reject) => {
153
+ let timeoutId;
154
+ let listener;
155
+
156
+ const cleanup = () => {
157
+ if (timeoutId) clearTimeout(timeoutId);
158
+ if (listener) listener.off();
159
+ };
160
+
161
+ listener = gunChain.on((data) => {
162
+ if (predicate(data)) {
163
+ cleanup();
164
+ resolve(data);
165
+ }
166
+ });
167
+
168
+ timeoutId = setTimeout(() => {
169
+ cleanup();
170
+ reject(new Error('Timeout waiting for condition'));
171
+ }, timeout);
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Batch read multiple Gun paths
177
+ * @param {Object} gun - Gun instance
178
+ * @param {string[]} paths - Array of paths to read
179
+ * @returns {Promise<Object>} Object mapping paths to data
180
+ */
181
+ export async function gunBatchGet(gun, paths) {
182
+ const promises = paths.map(async (path) => {
183
+ const data = await gunPromise(gun.get(path));
184
+ return [path, data];
185
+ });
186
+
187
+ const results = await Promise.all(promises);
188
+ return Object.fromEntries(results);
189
+ }
190
+
191
+ /**
192
+ * Batch write multiple Gun paths
193
+ * @param {Object} gun - Gun instance
194
+ * @param {Object} pathDataMap - Object mapping paths to data
195
+ * @returns {Promise<Object>} Object mapping paths to ack results
196
+ */
197
+ export async function gunBatchPut(gun, pathDataMap) {
198
+ const promises = Object.entries(pathDataMap).map(async ([path, data]) => {
199
+ const ack = await gunPut(gun.get(path), data);
200
+ return [path, ack];
201
+ });
202
+
203
+ const results = await Promise.all(promises);
204
+ return Object.fromEntries(results);
205
+ }
206
+
207
+ /**
208
+ * Retry Gun operation with exponential backoff
209
+ * @param {Function} operation - Async function to retry
210
+ * @param {number} maxRetries - Max retry attempts (default 3)
211
+ * @param {number} baseDelay - Base delay in ms (default 100ms)
212
+ * @returns {Promise<any>} Promise resolving to operation result
213
+ */
214
+ export async function gunRetry(operation, maxRetries = 3, baseDelay = 100) {
215
+ let lastError;
216
+
217
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
218
+ try {
219
+ return await operation();
220
+ } catch (error) {
221
+ lastError = error;
222
+ if (attempt < maxRetries) {
223
+ const delay = baseDelay * Math.pow(2, attempt);
224
+ await new Promise((resolve) => setTimeout(resolve, delay));
225
+ }
226
+ }
227
+ }
228
+
229
+ throw lastError;
230
+ }
231
+
232
+ /**
233
+ * Create async stream from Gun on() callback
234
+ * @param {Object} gunChain - Gun chain reference
235
+ * @returns {Object} Stream object with async iteration support
236
+ */
237
+ export function gunStream(gunChain) {
238
+ let listeners = [];
239
+ let buffer = [];
240
+ let ended = false;
241
+
242
+ const stream = {
243
+ [Symbol.asyncIterator]() {
244
+ return {
245
+ async next() {
246
+ if (buffer.length > 0) {
247
+ return { value: buffer.shift(), done: false };
248
+ }
249
+
250
+ if (ended) {
251
+ return { done: true };
252
+ }
253
+
254
+ // Wait for next value
255
+ return new Promise((resolve) => {
256
+ listeners.push((data) => {
257
+ resolve({ value: data, done: false });
258
+ });
259
+ });
260
+ },
261
+ };
262
+ },
263
+
264
+ stop() {
265
+ ended = true;
266
+ if (this.listener) this.listener.off();
267
+ },
268
+ };
269
+
270
+ stream.listener = gunChain.on((data, key) => {
271
+ const item = { key, data };
272
+ if (listeners.length > 0) {
273
+ const listener = listeners.shift();
274
+ listener(item);
275
+ } else {
276
+ buffer.push(item);
277
+ }
278
+ });
279
+
280
+ return stream;
281
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * GunDB Storage Wrapper with radisk persistence
3
+ * Handles path construction and CRUD operations
4
+ */
5
+
6
+ import { gunPromise, gunPut, gunCollect } from './gun-async.js';
7
+
8
+ /**
9
+ * Build Gun path from components
10
+ * @param {string} appname - Application namespace
11
+ * @param {string} holon - Holon ID (H3 or URI)
12
+ * @param {string} lens - Lens name
13
+ * @param {string} key - Data key (optional)
14
+ * @returns {string} Gun path
15
+ */
16
+ export function buildPath(appname, holon, lens, key = null) {
17
+ // Encode components to handle special characters
18
+ const encodedHolon = encodePathComponent(holon);
19
+ const encodedLens = encodePathComponent(lens);
20
+
21
+ if (key) {
22
+ const encodedKey = encodePathComponent(key);
23
+ return `${appname}/${encodedHolon}/${encodedLens}/${encodedKey}`;
24
+ }
25
+ return `${appname}/${encodedHolon}/${encodedLens}`;
26
+ }
27
+
28
+ /**
29
+ * Encode path component to handle special characters
30
+ * @private
31
+ */
32
+ function encodePathComponent(component) {
33
+ return encodeURIComponent(component).replace(/%2F/g, '/');
34
+ }
35
+
36
+ /**
37
+ * Write data to Gun with radisk persistence
38
+ * @param {Object} gun - Gun instance
39
+ * @param {string} path - Gun path
40
+ * @param {Object} data - Data to write
41
+ * @returns {Promise<boolean>} Success indicator
42
+ */
43
+ export async function write(gun, path, data) {
44
+ try {
45
+ await gunPut(gun.get(path), data, 2000);
46
+ // Delay to allow Gun to propagate the write (50ms for better reliability)
47
+ await new Promise(resolve => setTimeout(resolve, 50));
48
+ return true;
49
+ } catch (error) {
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Read data from Gun
56
+ * @param {Object} gun - Gun instance
57
+ * @param {string} path - Gun path
58
+ * @returns {Promise<Object|null>} Data or null if not found
59
+ */
60
+ export async function read(gun, path) {
61
+ const data = await gunPromise(gun.get(path), 2000);
62
+
63
+ // Return null if deleted or not found
64
+ if (!data || data._deleted) {
65
+ return null;
66
+ }
67
+
68
+ return data;
69
+ }
70
+
71
+ /**
72
+ * Read all data under a path (lens query)
73
+ * @param {Object} gun - Gun instance
74
+ * @param {string} path - Gun path
75
+ * @returns {Promise<Object[]>} Array of data objects
76
+ */
77
+ export async function readAll(gun, path) {
78
+ const results = await gunCollect(gun.get(path), 1000);
79
+
80
+ // Filter out deleted items and Gun metadata
81
+ return results
82
+ .filter(({ data, key }) => {
83
+ if (!data || typeof data !== 'object') return false;
84
+ if (key.startsWith('_')) return false;
85
+ if (data._deleted) return false;
86
+ return true;
87
+ })
88
+ .map(({ data }) => data);
89
+ }
90
+
91
+ /**
92
+ * Update data (merge fields)
93
+ * @param {Object} gun - Gun instance
94
+ * @param {string} path - Gun path
95
+ * @param {Object} updates - Fields to update
96
+ * @returns {Promise<boolean>} Success indicator
97
+ */
98
+ export async function update(gun, path, updates) {
99
+ const existing = await gunPromise(gun.get(path));
100
+
101
+ if (!existing || !existing.id || existing._deleted) {
102
+ return false; // Not found or deleted
103
+ }
104
+
105
+ // Remove Gun metadata before merging
106
+ const cleanExisting = { ...existing };
107
+ delete cleanExisting['_'];
108
+
109
+ // Merge updates
110
+ const merged = { ...cleanExisting, ...updates };
111
+
112
+ try {
113
+ await gunPut(gun.get(path), merged);
114
+ return true;
115
+ } catch (error) {
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Delete data (tombstone)
122
+ * @param {Object} gun - Gun instance
123
+ * @param {string} path - Gun path
124
+ * @returns {Promise<boolean>} Success indicator
125
+ */
126
+ export async function deleteData(gun, path) {
127
+ try {
128
+ // Gun requires tombstone to be an object with _deleted flag
129
+ // First read existing data to preserve metadata
130
+ const existing = await gunPromise(gun.get(path));
131
+ if (!existing) {
132
+ return true; // Already deleted/doesn't exist
133
+ }
134
+
135
+ // Create tombstone object
136
+ const tombstone = {
137
+ id: existing.id,
138
+ _deleted: true,
139
+ _deletedAt: Date.now()
140
+ };
141
+
142
+ await gunPut(gun.get(path), tombstone);
143
+ return true;
144
+ } catch (error) {
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Delete all data under path prefix (tombstone)
151
+ * @param {Object} gun - Gun instance
152
+ * @param {string} path - Gun path prefix
153
+ * @returns {Promise<Object>} Deletion results { success: boolean, count: number }
154
+ */
155
+ export async function deleteAll(gun, path) {
156
+ const items = await readAll(gun, path);
157
+ let count = 0;
158
+
159
+ for (const item of items) {
160
+ if (item && item.id) {
161
+ const itemPath = `${path}/${item.id}`;
162
+ await deleteData(gun, itemPath);
163
+ count++;
164
+ }
165
+ }
166
+
167
+ return { success: true, count };
168
+ }
169
+
170
+ /**
171
+ * Subscribe to data changes
172
+ * @param {Object} gun - Gun instance
173
+ * @param {string} path - Gun path
174
+ * @param {Function} callback - Called on data changes
175
+ * @param {Object} options - Subscription options
176
+ * @param {boolean} options.prefix - Subscribe to all items under path (default: auto-detect)
177
+ * @returns {Object} Subscription object with unsubscribe method
178
+ */
179
+ export function subscribe(gun, path, callback, options = {}) {
180
+ // Detect if this is a prefix subscription
181
+ const pathParts = path.split('/');
182
+ const isPrefix = options.prefix !== undefined ? options.prefix : pathParts.length <= 3;
183
+
184
+ if (isPrefix) {
185
+ // Subscribe to all items under this prefix
186
+ const ref = gun.get(path);
187
+
188
+ ref.map().on((data, key) => {
189
+ if (data && !key.startsWith('_') && !data._deleted) {
190
+ callback(data, key);
191
+ }
192
+ });
193
+
194
+ return {
195
+ unsubscribe: () => {
196
+ try {
197
+ ref.off();
198
+ } catch (e) {
199
+ // Ignore cleanup errors
200
+ }
201
+ },
202
+ };
203
+ } else {
204
+ // Subscribe to single item
205
+ const listener = gun.get(path).on((data, key) => {
206
+ if (data && !data._deleted) {
207
+ callback(data, key);
208
+ }
209
+ });
210
+
211
+ return {
212
+ unsubscribe: () => {
213
+ try {
214
+ listener.off();
215
+ } catch (e) {
216
+ // Ignore cleanup errors
217
+ }
218
+ },
219
+ };
220
+ }
221
+ }