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.
Files changed (147) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/LICENSE +162 -38
  5. package/README.md +483 -367
  6. package/bin/holosphere-activitypub.js +158 -0
  7. package/cleanup-test-data.js +204 -0
  8. package/examples/demo.html +1333 -0
  9. package/examples/example-bot.js +197 -0
  10. package/package.json +47 -87
  11. package/scripts/check-bundle-size.js +54 -0
  12. package/scripts/check-quest-ids.js +77 -0
  13. package/scripts/import-holons.js +578 -0
  14. package/scripts/publish-to-relay.js +101 -0
  15. package/scripts/read-example.js +186 -0
  16. package/scripts/relay-diagnostic.js +59 -0
  17. package/scripts/relay-example.js +179 -0
  18. package/scripts/resync-to-relay.js +245 -0
  19. package/scripts/revert-import.js +196 -0
  20. package/scripts/test-hybrid-mode.js +108 -0
  21. package/scripts/test-local-storage.js +63 -0
  22. package/scripts/test-nostr-direct.js +55 -0
  23. package/scripts/test-read-data.js +45 -0
  24. package/scripts/test-write-read.js +63 -0
  25. package/scripts/verify-import.js +95 -0
  26. package/scripts/verify-relay-data.js +139 -0
  27. package/src/ai/aggregation.js +319 -0
  28. package/src/ai/breakdown.js +511 -0
  29. package/src/ai/classifier.js +217 -0
  30. package/src/ai/council.js +228 -0
  31. package/src/ai/embeddings.js +279 -0
  32. package/src/ai/federation-ai.js +324 -0
  33. package/src/ai/h3-ai.js +955 -0
  34. package/src/ai/index.js +112 -0
  35. package/src/ai/json-ops.js +225 -0
  36. package/src/ai/llm-service.js +205 -0
  37. package/src/ai/nl-query.js +223 -0
  38. package/src/ai/relationships.js +353 -0
  39. package/src/ai/schema-extractor.js +218 -0
  40. package/src/ai/spatial.js +293 -0
  41. package/src/ai/tts.js +194 -0
  42. package/src/content/social-protocols.js +168 -0
  43. package/src/core/holosphere.js +273 -0
  44. package/src/crypto/secp256k1.js +259 -0
  45. package/src/federation/discovery.js +334 -0
  46. package/src/federation/hologram.js +1042 -0
  47. package/src/federation/registry.js +386 -0
  48. package/src/hierarchical/upcast.js +110 -0
  49. package/src/index.js +2669 -0
  50. package/src/schema/validator.js +91 -0
  51. package/src/spatial/h3-operations.js +110 -0
  52. package/src/storage/backend-factory.js +125 -0
  53. package/src/storage/backend-interface.js +142 -0
  54. package/src/storage/backends/activitypub/server.js +653 -0
  55. package/src/storage/backends/activitypub-backend.js +272 -0
  56. package/src/storage/backends/gundb-backend.js +233 -0
  57. package/src/storage/backends/nostr-backend.js +136 -0
  58. package/src/storage/filesystem-storage-browser.js +41 -0
  59. package/src/storage/filesystem-storage.js +138 -0
  60. package/src/storage/global-tables.js +81 -0
  61. package/src/storage/gun-async.js +281 -0
  62. package/src/storage/gun-wrapper.js +221 -0
  63. package/src/storage/indexeddb-storage.js +122 -0
  64. package/src/storage/key-storage-simple.js +76 -0
  65. package/src/storage/key-storage.js +136 -0
  66. package/src/storage/memory-storage.js +59 -0
  67. package/src/storage/migration.js +338 -0
  68. package/src/storage/nostr-async.js +811 -0
  69. package/src/storage/nostr-client.js +939 -0
  70. package/src/storage/nostr-wrapper.js +211 -0
  71. package/src/storage/outbox-queue.js +208 -0
  72. package/src/storage/persistent-storage.js +109 -0
  73. package/src/storage/sync-service.js +164 -0
  74. package/src/subscriptions/manager.js +142 -0
  75. package/test-ai-real-api.js +202 -0
  76. package/tests/unit/ai/aggregation.test.js +295 -0
  77. package/tests/unit/ai/breakdown.test.js +446 -0
  78. package/tests/unit/ai/classifier.test.js +294 -0
  79. package/tests/unit/ai/council.test.js +262 -0
  80. package/tests/unit/ai/embeddings.test.js +384 -0
  81. package/tests/unit/ai/federation-ai.test.js +344 -0
  82. package/tests/unit/ai/h3-ai.test.js +458 -0
  83. package/tests/unit/ai/index.test.js +304 -0
  84. package/tests/unit/ai/json-ops.test.js +307 -0
  85. package/tests/unit/ai/llm-service.test.js +390 -0
  86. package/tests/unit/ai/nl-query.test.js +383 -0
  87. package/tests/unit/ai/relationships.test.js +311 -0
  88. package/tests/unit/ai/schema-extractor.test.js +384 -0
  89. package/tests/unit/ai/spatial.test.js +279 -0
  90. package/tests/unit/ai/tts.test.js +279 -0
  91. package/tests/unit/content.test.js +332 -0
  92. package/tests/unit/contract/core.test.js +88 -0
  93. package/tests/unit/contract/crypto.test.js +198 -0
  94. package/tests/unit/contract/data.test.js +223 -0
  95. package/tests/unit/contract/federation.test.js +181 -0
  96. package/tests/unit/contract/hierarchical.test.js +113 -0
  97. package/tests/unit/contract/schema.test.js +114 -0
  98. package/tests/unit/contract/social.test.js +217 -0
  99. package/tests/unit/contract/spatial.test.js +110 -0
  100. package/tests/unit/contract/subscriptions.test.js +128 -0
  101. package/tests/unit/contract/utils.test.js +159 -0
  102. package/tests/unit/core.test.js +152 -0
  103. package/tests/unit/crypto.test.js +328 -0
  104. package/tests/unit/federation.test.js +234 -0
  105. package/tests/unit/gun-async.test.js +252 -0
  106. package/tests/unit/hierarchical.test.js +399 -0
  107. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  108. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  109. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  110. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  111. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  112. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  113. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  114. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  115. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  116. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  117. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  118. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  119. package/tests/unit/performance/benchmark.test.js +85 -0
  120. package/tests/unit/schema.test.js +213 -0
  121. package/tests/unit/spatial.test.js +158 -0
  122. package/tests/unit/storage.test.js +195 -0
  123. package/tests/unit/subscriptions.test.js +328 -0
  124. package/tests/unit/test-data-permanence-debug.js +197 -0
  125. package/tests/unit/test-data-permanence.js +340 -0
  126. package/tests/unit/test-key-persistence-fixed.js +148 -0
  127. package/tests/unit/test-key-persistence.js +172 -0
  128. package/tests/unit/test-relay-permanence.js +376 -0
  129. package/tests/unit/test-second-node.js +95 -0
  130. package/tests/unit/test-simple-write.js +89 -0
  131. package/vite.config.js +49 -0
  132. package/vitest.config.js +20 -0
  133. package/FEDERATION.md +0 -213
  134. package/compute.js +0 -298
  135. package/content.js +0 -980
  136. package/federation.js +0 -1234
  137. package/global.js +0 -736
  138. package/hexlib.js +0 -335
  139. package/hologram.js +0 -183
  140. package/holosphere-bundle.esm.js +0 -33256
  141. package/holosphere-bundle.js +0 -33287
  142. package/holosphere-bundle.min.js +0 -39
  143. package/holosphere.d.ts +0 -601
  144. package/holosphere.js +0 -719
  145. package/node.js +0 -246
  146. package/schema.js +0 -139
  147. package/utils.js +0 -302
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Nostr Storage Wrapper
3
+ * Drop-in replacement for gun-wrapper.js with same API
4
+ * Handles path construction and CRUD operations using Nostr relays
5
+ */
6
+
7
+ import {
8
+ nostrPut,
9
+ nostrGet,
10
+ nostrGetAll,
11
+ nostrGetAllHybrid,
12
+ nostrUpdate as nostrAsyncUpdate,
13
+ nostrDelete,
14
+ nostrDeleteAll,
15
+ nostrSubscribe,
16
+ nostrSubscribeMany,
17
+ } from './nostr-async.js';
18
+
19
+ /**
20
+ * Build path from components
21
+ * @param {string} appname - Application namespace
22
+ * @param {string} holon - Holon ID (H3 or URI)
23
+ * @param {string} lens - Lens name
24
+ * @param {string} key - Data key (optional)
25
+ * @returns {string} Path string
26
+ */
27
+ export function buildPath(appname, holon, lens, key = null) {
28
+ // Encode components to handle special characters
29
+ const encodedHolon = encodePathComponent(holon);
30
+ const encodedLens = encodePathComponent(lens);
31
+
32
+ if (key) {
33
+ const encodedKey = encodePathComponent(key);
34
+ return `${appname}/${encodedHolon}/${encodedLens}/${encodedKey}`;
35
+ }
36
+ return `${appname}/${encodedHolon}/${encodedLens}`;
37
+ }
38
+
39
+ /**
40
+ * Encode path component to handle special characters
41
+ * @private
42
+ */
43
+ function encodePathComponent(component) {
44
+ return encodeURIComponent(component).replace(/%2F/g, '/');
45
+ }
46
+
47
+ /**
48
+ * Write data to Nostr
49
+ * @param {Object} client - NostrClient instance
50
+ * @param {string} path - Path
51
+ * @param {Object} data - Data to write
52
+ * @returns {Promise<boolean>} Success indicator
53
+ */
54
+ export async function write(client, path, data) {
55
+ try {
56
+ const result = await nostrPut(client, path, data);
57
+ // Check if at least one relay accepted the event
58
+ // If no relays (testing mode), consider it successful if event was created
59
+ const success = result.results.length === 0 ? true : result.results.some(r => r.status === 'fulfilled');
60
+ return success;
61
+ } catch (error) {
62
+ console.error('Nostr write error:', error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Read data from Nostr
69
+ * @param {Object} client - NostrClient instance
70
+ * @param {string} path - Path
71
+ * @param {Object} options - Query options
72
+ * @param {string[]} options.authors - Array of public keys to query (for cross-holosphere reads)
73
+ * @param {boolean} options.includeAuthor - If true, adds _author field to returned data
74
+ * @returns {Promise<Object|null>} Data or null if not found
75
+ */
76
+ export async function read(client, path, options = {}) {
77
+ const data = await nostrGet(client, path, 30000, options);
78
+
79
+ // Return null if deleted or not found
80
+ if (!data || data._deleted) {
81
+ return null;
82
+ }
83
+
84
+ return data;
85
+ }
86
+
87
+ /**
88
+ * Read all data under a path (lens query)
89
+ * @param {Object} client - NostrClient instance
90
+ * @param {string} path - Path prefix
91
+ * @param {Object} options - Query options
92
+ * @param {boolean} options.hybrid - Use hybrid mode (local + relay) - default: false
93
+ * @param {string[]} options.authors - Array of public keys to query (for cross-holosphere reads)
94
+ * @param {boolean} options.includeAuthor - If true, adds _author field to returned data
95
+ * @returns {Promise<Object[]>} Array of data objects
96
+ */
97
+ export async function readAll(client, path, options = {}) {
98
+ // Use hybrid mode if enabled and client supports it
99
+ const useHybrid = options.hybrid && typeof client.queryHybrid === 'function';
100
+ const queryFn = useHybrid ? nostrGetAllHybrid : nostrGetAll;
101
+
102
+ const results = await queryFn(client, path, 30000, options);
103
+
104
+ // Filter out deleted items
105
+ return results.filter(data => {
106
+ if (!data || typeof data !== 'object') return false;
107
+ if (data._deleted) return false;
108
+ return true;
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Update data (merge fields)
114
+ * @param {Object} client - NostrClient instance
115
+ * @param {string} path - Path
116
+ * @param {Object} updates - Fields to update
117
+ * @returns {Promise<boolean>} Success indicator
118
+ */
119
+ export async function update(client, path, updates) {
120
+ try {
121
+ const existing = await nostrGet(client, path);
122
+
123
+ if (!existing || !existing.id || existing._deleted) {
124
+ return false; // Not found or deleted
125
+ }
126
+
127
+ // Merge updates
128
+ const merged = { ...existing, ...updates };
129
+
130
+ // Update timestamp
131
+ if (!merged._meta) merged._meta = {};
132
+ merged._meta.timestamp = Date.now();
133
+
134
+ // Write merged data
135
+ const result = await nostrPut(client, path, merged);
136
+ // If no relays (testing mode), consider it successful if event was created
137
+ const success = result.results.length === 0 ? true : result.results.some(r => r.status === 'fulfilled');
138
+ return success;
139
+ } catch (error) {
140
+ console.error('Nostr update error:', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Delete data (tombstone)
147
+ * @param {Object} client - NostrClient instance
148
+ * @param {string} path - Path
149
+ * @returns {Promise<boolean>} Success indicator
150
+ */
151
+ export async function deleteData(client, path) {
152
+ try {
153
+ const result = await nostrDelete(client, path);
154
+
155
+ if (result.reason === 'not_found') {
156
+ return false; // Item doesn't exist
157
+ }
158
+
159
+ // If no relays (testing mode), consider it successful if event was created
160
+ const success = result.results.length === 0 ? true : result.results.some(r => r.status === 'fulfilled');
161
+ return success;
162
+ } catch (error) {
163
+ console.error('Nostr delete error:', error);
164
+ throw error;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Delete all data under path prefix (tombstone)
170
+ * @param {Object} client - NostrClient instance
171
+ * @param {string} path - Path prefix
172
+ * @returns {Promise<Object>} Deletion results
173
+ */
174
+ export async function deleteAll(client, path) {
175
+ try {
176
+ const result = await nostrDeleteAll(client, path);
177
+ return result;
178
+ } catch (error) {
179
+ console.error('Nostr deleteAll error:', error);
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Subscribe to data changes
186
+ * @param {Object} client - NostrClient instance
187
+ * @param {string} path - Path
188
+ * @param {Function} callback - Called on data changes
189
+ * @param {Object} options - Subscription options
190
+ * @param {boolean} options.realtimeOnly - Only receive new events (default: true)
191
+ * @returns {Promise<Object>} Subscription object with unsubscribe method
192
+ */
193
+ export async function subscribe(client, path, callback, options = {}) {
194
+ // Determine if this is a prefix subscription (no key) or single item
195
+ const pathParts = path.split('/');
196
+ const isPrefix = pathParts.length <= 3; // appname/holon/lens
197
+
198
+ if (isPrefix) {
199
+ // Subscribe to all items under this prefix
200
+ return await nostrSubscribeMany(client, path + '/', (data, fullPath) => {
201
+ // Extract key from full path
202
+ const key = fullPath.split('/').pop();
203
+ callback(data, key);
204
+ }, options);
205
+ } else {
206
+ // Subscribe to single item
207
+ return nostrSubscribe(client, path, (data) => {
208
+ callback(data, path.split('/').pop());
209
+ }, options);
210
+ }
211
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * OutboxQueue - Persistent queue for reliable relay delivery
3
+ *
4
+ * Implements guaranteed delivery with exponential backoff retry.
5
+ * Events are persisted to local storage and retried until delivered
6
+ * or max retries exceeded.
7
+ */
8
+
9
+ export class OutboxQueue {
10
+ /**
11
+ * @param {Object} persistentStorage - Storage adapter (IndexedDB/FileSystem)
12
+ * @param {Object} options - Configuration options
13
+ * @param {number} options.maxRetries - Max retry attempts (default: 5)
14
+ * @param {number} options.baseDelay - Base delay in ms (default: 1000)
15
+ * @param {number} options.maxDelay - Max delay cap in ms (default: 60000)
16
+ * @param {number} options.failedTTL - TTL for failed events in ms (default: 24 hours)
17
+ */
18
+ constructor(persistentStorage, options = {}) {
19
+ this.storage = persistentStorage;
20
+ this.queuePrefix = '_outbox/';
21
+ this.maxRetries = options.maxRetries || 5;
22
+ this.baseDelay = options.baseDelay || 1000; // 1 second
23
+ this.maxDelay = options.maxDelay || 60000; // 1 minute
24
+ this.failedTTL = options.failedTTL || 24 * 60 * 60 * 1000; // 24 hours
25
+ }
26
+
27
+ /**
28
+ * Add an event to the outbox queue
29
+ * @param {Object} event - Signed Nostr event
30
+ * @param {string[]} relays - Target relay URLs
31
+ * @returns {Promise<Object>} Queue entry
32
+ */
33
+ async enqueue(event, relays) {
34
+ const entry = {
35
+ id: event.id,
36
+ event,
37
+ relays,
38
+ status: 'pending',
39
+ retries: 0,
40
+ createdAt: Date.now(),
41
+ nextRetryAt: Date.now(),
42
+ failedRelays: [],
43
+ };
44
+
45
+ const key = `${this.queuePrefix}${event.id}`;
46
+ await this.storage.put(key, entry);
47
+ return entry;
48
+ }
49
+
50
+ /**
51
+ * Mark an event as sent (removes from queue if successful)
52
+ * @param {string} eventId - Event ID
53
+ * @param {string[]} successfulRelays - Relays that accepted the event
54
+ */
55
+ async markSent(eventId, successfulRelays) {
56
+ const key = `${this.queuePrefix}${eventId}`;
57
+ const entry = await this.storage.get(key);
58
+ if (!entry) return;
59
+
60
+ if (successfulRelays.length > 0) {
61
+ // At least one relay accepted - remove from queue
62
+ await this.storage.delete(key);
63
+ } else {
64
+ // All relays failed - schedule retry
65
+ await this._scheduleRetry(key, entry);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Schedule a retry with exponential backoff
71
+ * @private
72
+ */
73
+ async _scheduleRetry(key, entry) {
74
+ entry.retries++;
75
+
76
+ if (entry.retries >= this.maxRetries) {
77
+ entry.status = 'failed';
78
+ entry.failedAt = Date.now();
79
+ } else {
80
+ entry.status = 'pending';
81
+ // Exponential backoff with jitter
82
+ const delay = Math.min(
83
+ this.baseDelay * Math.pow(2, entry.retries) + Math.random() * 1000,
84
+ this.maxDelay
85
+ );
86
+ entry.nextRetryAt = Date.now() + delay;
87
+ }
88
+
89
+ await this.storage.put(key, entry);
90
+ }
91
+
92
+ /**
93
+ * Get all pending events ready for retry
94
+ * @returns {Promise<Object[]>} Pending queue entries sorted by creation time
95
+ */
96
+ async getPendingEvents() {
97
+ const allEntries = await this.storage.getAll(this.queuePrefix);
98
+ const now = Date.now();
99
+
100
+ return allEntries
101
+ .filter(e => e && e.status === 'pending' && e.nextRetryAt <= now)
102
+ .sort((a, b) => a.createdAt - b.createdAt);
103
+ }
104
+
105
+ /**
106
+ * Get all failed events (exceeded max retries)
107
+ * @returns {Promise<Object[]>} Failed queue entries
108
+ */
109
+ async getFailedEvents() {
110
+ const allEntries = await this.storage.getAll(this.queuePrefix);
111
+ return allEntries.filter(e => e && e.status === 'failed');
112
+ }
113
+
114
+ /**
115
+ * Get queue statistics
116
+ * @returns {Promise<Object>} Stats object
117
+ */
118
+ async getStats() {
119
+ const allEntries = await this.storage.getAll(this.queuePrefix);
120
+
121
+ const stats = {
122
+ total: allEntries.length,
123
+ pending: 0,
124
+ failed: 0,
125
+ oldestPending: null,
126
+ oldestFailed: null,
127
+ };
128
+
129
+ for (const entry of allEntries) {
130
+ if (!entry) continue;
131
+
132
+ if (entry.status === 'pending') {
133
+ stats.pending++;
134
+ if (!stats.oldestPending || entry.createdAt < stats.oldestPending) {
135
+ stats.oldestPending = entry.createdAt;
136
+ }
137
+ } else if (entry.status === 'failed') {
138
+ stats.failed++;
139
+ if (!stats.oldestFailed || entry.failedAt < stats.oldestFailed) {
140
+ stats.oldestFailed = entry.failedAt;
141
+ }
142
+ }
143
+ }
144
+
145
+ return stats;
146
+ }
147
+
148
+ /**
149
+ * Purge failed events older than maxAge
150
+ * @param {number} maxAge - Max age in ms (default: 24 hours)
151
+ * @returns {Promise<number>} Number of entries purged
152
+ */
153
+ async purgeOldFailed(maxAge = this.failedTTL) {
154
+ const allEntries = await this.storage.getAll(this.queuePrefix);
155
+ const now = Date.now();
156
+ let purged = 0;
157
+
158
+ for (const entry of allEntries) {
159
+ if (entry && entry.status === 'failed' && (now - entry.failedAt) > maxAge) {
160
+ await this.storage.delete(`${this.queuePrefix}${entry.id}`);
161
+ purged++;
162
+ }
163
+ }
164
+
165
+ return purged;
166
+ }
167
+
168
+ /**
169
+ * Clear all entries from the queue
170
+ * @returns {Promise<number>} Number of entries cleared
171
+ */
172
+ async clear() {
173
+ const allEntries = await this.storage.getAll(this.queuePrefix);
174
+ let cleared = 0;
175
+
176
+ for (const entry of allEntries) {
177
+ if (entry && entry.id) {
178
+ await this.storage.delete(`${this.queuePrefix}${entry.id}`);
179
+ cleared++;
180
+ }
181
+ }
182
+
183
+ return cleared;
184
+ }
185
+
186
+ /**
187
+ * Manually retry a failed event
188
+ * @param {string} eventId - Event ID to retry
189
+ * @returns {Promise<Object|null>} Updated entry or null if not found
190
+ */
191
+ async retryFailed(eventId) {
192
+ const key = `${this.queuePrefix}${eventId}`;
193
+ const entry = await this.storage.get(key);
194
+
195
+ if (!entry || entry.status !== 'failed') {
196
+ return null;
197
+ }
198
+
199
+ // Reset for retry
200
+ entry.status = 'pending';
201
+ entry.retries = 0;
202
+ entry.nextRetryAt = Date.now();
203
+ delete entry.failedAt;
204
+
205
+ await this.storage.put(key, entry);
206
+ return entry;
207
+ }
208
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Persistent Storage Interface
3
+ * Provides a unified API for local persistence across browser and Node.js
4
+ */
5
+
6
+ /**
7
+ * Base class for persistent storage adapters
8
+ */
9
+ class PersistentStorage {
10
+ /**
11
+ * Initialize storage
12
+ * @param {string} namespace - Storage namespace (appName)
13
+ */
14
+ async init(namespace) {
15
+ throw new Error('init() must be implemented by subclass');
16
+ }
17
+
18
+ /**
19
+ * Store event
20
+ * @param {string} key - Event ID or d-tag
21
+ * @param {Object} event - Nostr event
22
+ */
23
+ async put(key, event) {
24
+ throw new Error('put() must be implemented by subclass');
25
+ }
26
+
27
+ /**
28
+ * Retrieve event
29
+ * @param {string} key - Event ID or d-tag
30
+ * @returns {Promise<Object|null>} Event or null
31
+ */
32
+ async get(key) {
33
+ throw new Error('get() must be implemented by subclass');
34
+ }
35
+
36
+ /**
37
+ * Get all events matching prefix
38
+ * @param {string} prefix - Key prefix
39
+ * @returns {Promise<Object[]>} Array of events
40
+ */
41
+ async getAll(prefix) {
42
+ throw new Error('getAll() must be implemented by subclass');
43
+ }
44
+
45
+ /**
46
+ * Delete event
47
+ * @param {string} key - Event ID or d-tag
48
+ */
49
+ async delete(key) {
50
+ throw new Error('delete() must be implemented by subclass');
51
+ }
52
+
53
+ /**
54
+ * Clear all data in namespace
55
+ */
56
+ async clear() {
57
+ throw new Error('clear() must be implemented by subclass');
58
+ }
59
+
60
+ /**
61
+ * Close storage
62
+ */
63
+ async close() {
64
+ throw new Error('close() must be implemented by subclass');
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Detect environment and return appropriate storage adapter
70
+ * @param {string} namespace - Storage namespace
71
+ * @param {Object} options - Storage options
72
+ * @returns {Promise<PersistentStorage>} Storage instance
73
+ */
74
+ async function createPersistentStorage(namespace, options = {}) {
75
+ // Detect environment
76
+ const isBrowser = typeof window !== 'undefined' && typeof window.indexedDB !== 'undefined';
77
+ const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
78
+
79
+ if (isBrowser) {
80
+ // Use IndexedDB in browser
81
+ const { IndexedDBStorage } = await import('./indexeddb-storage.js');
82
+ const storage = new IndexedDBStorage();
83
+ await storage.init(namespace);
84
+ return storage;
85
+ } else if (isNode) {
86
+ // Use file system in Node.js
87
+ try {
88
+ const { FileSystemStorage } = await import('./filesystem-storage.js');
89
+ const storage = new FileSystemStorage(options.dataDir);
90
+ await storage.init(namespace);
91
+ return storage;
92
+ } catch (error) {
93
+ // Fall back to memory storage if filesystem module fails to load
94
+ console.warn('FileSystemStorage not available, using MemoryStorage instead');
95
+ const { MemoryStorage } = await import('./memory-storage.js');
96
+ const storage = new MemoryStorage();
97
+ await storage.init(namespace);
98
+ return storage;
99
+ }
100
+ } else {
101
+ // Fallback to memory-only storage
102
+ const { MemoryStorage } = await import('./memory-storage.js');
103
+ const storage = new MemoryStorage();
104
+ await storage.init(namespace);
105
+ return storage;
106
+ }
107
+ }
108
+
109
+ export { PersistentStorage, createPersistentStorage };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * SyncService - Background service for reliable sync
3
+ *
4
+ * Periodically processes the outbox queue to retry failed deliveries
5
+ * and purge old failed events.
6
+ */
7
+
8
+ export class SyncService {
9
+ /**
10
+ * @param {Object} client - NostrClient instance
11
+ * @param {Object} options - Configuration options
12
+ * @param {number} options.interval - Sync interval in ms (default: 10000)
13
+ * @param {boolean} options.autoStart - Start automatically (default: false)
14
+ */
15
+ constructor(client, options = {}) {
16
+ this.client = client;
17
+ this.interval = options.interval || 10000; // 10 seconds
18
+ this.running = false;
19
+ this.timer = null;
20
+ this._processing = false;
21
+
22
+ if (options.autoStart) {
23
+ this.start();
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Start the background sync service
29
+ */
30
+ start() {
31
+ if (this.running) return;
32
+ this.running = true;
33
+ this._scheduleNextRun();
34
+ }
35
+
36
+ /**
37
+ * Stop the background sync service
38
+ */
39
+ stop() {
40
+ this.running = false;
41
+ if (this.timer) {
42
+ clearTimeout(this.timer);
43
+ this.timer = null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if the service is running
49
+ * @returns {boolean}
50
+ */
51
+ isRunning() {
52
+ return this.running;
53
+ }
54
+
55
+ /**
56
+ * Force an immediate sync (useful for testing or manual trigger)
57
+ * @returns {Promise<Object>} Sync result
58
+ */
59
+ async syncNow() {
60
+ return this._processOutbox();
61
+ }
62
+
63
+ /**
64
+ * Schedule the next run
65
+ * @private
66
+ */
67
+ _scheduleNextRun() {
68
+ if (!this.running) return;
69
+
70
+ this.timer = setTimeout(() => {
71
+ this._runLoop();
72
+ }, this.interval);
73
+ }
74
+
75
+ /**
76
+ * Main loop iteration
77
+ * @private
78
+ */
79
+ async _runLoop() {
80
+ if (!this.running) return;
81
+
82
+ try {
83
+ await this._processOutbox();
84
+ } catch (error) {
85
+ console.warn('[sync] Outbox processing failed:', error.message);
86
+ }
87
+
88
+ // Schedule next run
89
+ this._scheduleNextRun();
90
+ }
91
+
92
+ /**
93
+ * Process pending events in the outbox
94
+ * @private
95
+ * @returns {Promise<Object>} Processing result
96
+ */
97
+ async _processOutbox() {
98
+ // Prevent concurrent processing
99
+ if (this._processing) {
100
+ return { skipped: true, reason: 'already processing' };
101
+ }
102
+
103
+ this._processing = true;
104
+
105
+ const result = {
106
+ processed: 0,
107
+ succeeded: 0,
108
+ failed: 0,
109
+ purged: 0,
110
+ };
111
+
112
+ try {
113
+ if (!this.client.outboxQueue) {
114
+ return { ...result, skipped: true, reason: 'no outbox queue' };
115
+ }
116
+
117
+ // Get pending events ready for retry
118
+ const pending = await this.client.outboxQueue.getPendingEvents();
119
+ result.processed = pending.length;
120
+
121
+ // Process each pending event
122
+ for (const entry of pending) {
123
+ try {
124
+ const deliveryResult = await this.client._attemptDelivery(entry.event, entry.relays);
125
+
126
+ if (deliveryResult.successful.length > 0) {
127
+ result.succeeded++;
128
+ } else {
129
+ result.failed++;
130
+ }
131
+ } catch (error) {
132
+ console.warn(`[sync] Retry failed for ${entry.id}:`, error.message);
133
+ result.failed++;
134
+ }
135
+ }
136
+
137
+ // Purge old failed events (24 hour TTL)
138
+ result.purged = await this.client.outboxQueue.purgeOldFailed();
139
+
140
+ } finally {
141
+ this._processing = false;
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Get sync service statistics
149
+ * @returns {Promise<Object>} Stats including queue status
150
+ */
151
+ async getStats() {
152
+ const stats = {
153
+ running: this.running,
154
+ interval: this.interval,
155
+ queue: null,
156
+ };
157
+
158
+ if (this.client.outboxQueue) {
159
+ stats.queue = await this.client.outboxQueue.getStats();
160
+ }
161
+
162
+ return stats;
163
+ }
164
+ }