holosphere 2.0.0-alpha7 → 2.0.0-alpha8

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 (40) hide show
  1. package/dist/cjs/holosphere.cjs +1 -1
  2. package/dist/esm/holosphere.js +1 -1
  3. package/dist/{index-d6f4RJBM.js → index-4XHHKe6S.js} +356 -58
  4. package/dist/index-4XHHKe6S.js.map +1 -0
  5. package/dist/{index-jmTHEbR2.js → index-BjP1TXGz.js} +2 -2
  6. package/dist/{index-jmTHEbR2.js.map → index-BjP1TXGz.js.map} +1 -1
  7. package/dist/{index-C-IlLYlk.cjs → index-CKffQDmQ.cjs} +2 -2
  8. package/dist/{index-C-IlLYlk.cjs.map → index-CKffQDmQ.cjs.map} +1 -1
  9. package/dist/index-Dz5kOZMI.cjs +5 -0
  10. package/dist/index-Dz5kOZMI.cjs.map +1 -0
  11. package/dist/{indexeddb-storage-a8GipaDr.cjs → indexeddb-storage-DD7EFBVc.cjs} +2 -2
  12. package/dist/{indexeddb-storage-a8GipaDr.cjs.map → indexeddb-storage-DD7EFBVc.cjs.map} +1 -1
  13. package/dist/{indexeddb-storage-D8kOl0oK.js → indexeddb-storage-lExjjFlV.js} +2 -2
  14. package/dist/{indexeddb-storage-D8kOl0oK.js.map → indexeddb-storage-lExjjFlV.js.map} +1 -1
  15. package/dist/{memory-storage-DBQK622V.js → memory-storage-C68adso2.js} +2 -2
  16. package/dist/{memory-storage-DBQK622V.js.map → memory-storage-C68adso2.js.map} +1 -1
  17. package/dist/{memory-storage-gfRovk2O.cjs → memory-storage-DD_6yyXT.cjs} +2 -2
  18. package/dist/{memory-storage-gfRovk2O.cjs.map → memory-storage-DD_6yyXT.cjs.map} +1 -1
  19. package/dist/{secp256k1-BCAPF45D.cjs → secp256k1-DYELiqgx.cjs} +2 -2
  20. package/dist/{secp256k1-BCAPF45D.cjs.map → secp256k1-DYELiqgx.cjs.map} +1 -1
  21. package/dist/{secp256k1-DYm_CMqW.js → secp256k1-OM8siPyy.js} +2 -2
  22. package/dist/{secp256k1-DYm_CMqW.js.map → secp256k1-OM8siPyy.js.map} +1 -1
  23. package/examples/holosphere-widget.js +1242 -0
  24. package/examples/widget-demo.html +274 -0
  25. package/examples/widget.html +703 -0
  26. package/package.json +3 -1
  27. package/src/cdn-entry.js +22 -0
  28. package/src/contracts/queries.js +16 -1
  29. package/src/core/holosphere.js +2 -2
  30. package/src/crypto/nostr-utils.js +36 -2
  31. package/src/federation/handshake.js +16 -4
  32. package/src/index.js +16 -2
  33. package/src/storage/backends/gundb-backend.js +293 -9
  34. package/src/storage/gun-wrapper.js +64 -16
  35. package/src/storage/nostr-async.js +40 -25
  36. package/src/storage/unified-storage.js +31 -1
  37. package/vite.config.cdn.js +60 -0
  38. package/dist/index-Bvwyvd0T.cjs +0 -5
  39. package/dist/index-Bvwyvd0T.cjs.map +0 -1
  40. package/dist/index-d6f4RJBM.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "holosphere",
3
- "version": "2.0.0-alpha7",
3
+ "version": "2.0.0-alpha8",
4
4
  "description": "Holonic geospatial communication infrastructure combining H3 hexagonal indexing with distributed P2P storage",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,8 @@
19
19
  "scripts": {
20
20
  "dev": "vite",
21
21
  "build": "vite build",
22
+ "build:cdn": "vite build --config vite.config.cdn.js",
23
+ "build:all": "npm run build && npm run build:cdn",
22
24
  "test": "vitest run",
23
25
  "test:watch": "vitest",
24
26
  "test:coverage": "vitest run --coverage",
@@ -0,0 +1,22 @@
1
+ /**
2
+ * CDN Entry Point for HoloSphere
3
+ *
4
+ * This file creates a clean global API for browser usage.
5
+ * After including the script, use: new HoloSphere({ appName: 'my-app' })
6
+ *
7
+ * Also exposes h3 library as window.h3 for hexagon visualization.
8
+ */
9
+
10
+ // Import the main class
11
+ import { HoloSphere } from './index.js';
12
+
13
+ // Import h3-js for hexagon boundary visualization
14
+ import * as h3 from 'h3-js';
15
+
16
+ // Expose h3 globally for widget use
17
+ if (typeof window !== 'undefined') {
18
+ window.h3 = h3;
19
+ }
20
+
21
+ // Export only the main class as default
22
+ export default HoloSphere;
@@ -6,7 +6,22 @@
6
6
  */
7
7
 
8
8
  import { ethers } from 'ethers';
9
- import { ContractABIs } from './index.js';
9
+ // Import ABIs directly to avoid circular dependency with index.js
10
+ import SplitterABI from './abis/Splitter.json' with { type: 'json' };
11
+ import ManagedABI from './abis/Managed.json' with { type: 'json' };
12
+ import ZonedABI from './abis/Zoned.json' with { type: 'json' };
13
+ import AppreciativeABI from './abis/Appreciative.json' with { type: 'json' };
14
+ import BundleABI from './abis/Bundle.json' with { type: 'json' };
15
+ import HolonsABI from './abis/Holons.json' with { type: 'json' };
16
+
17
+ const ContractABIs = {
18
+ Splitter: SplitterABI,
19
+ Managed: ManagedABI,
20
+ Zoned: ZonedABI,
21
+ Appreciative: AppreciativeABI,
22
+ Bundle: BundleABI,
23
+ Holons: HolonsABI
24
+ };
10
25
 
11
26
  /**
12
27
  * Helper to format wei to ETH
@@ -61,7 +61,7 @@ export class HoloSphere {
61
61
  * @param {string} config.backend - Storage backend: 'nostr' | 'gundb' | 'activitypub' (default: 'nostr')
62
62
  * @param {string[]} config.relays - Nostr relay URLs (default from HOLOSPHERE_RELAYS env or ['wss://relay.holons.io', 'wss://relay.nostr.band'])
63
63
  * @param {string} config.privateKey - Private key for signing (hex format, optional)
64
- * @param {string} config.logLevel - Log verbosity: ERROR|WARN|INFO|DEBUG (default: 'WARN')
64
+ * @param {string} config.logLevel - Log verbosity: ERROR|WARN|INFO|DEBUG (default: 'INFO')
65
65
  * @param {boolean} config.hybridMode - Enable hybrid mode (local + relay queries) (default: true)
66
66
  * @param {Object} config.gundb - GunDB-specific configuration
67
67
  * @param {Object} config.activitypub - ActivityPub-specific configuration
@@ -97,7 +97,7 @@ export class HoloSphere {
97
97
  backend: config.backend || 'nostr',
98
98
  relays: config.relays || getDefaultRelays(),
99
99
  privateKey: config.privateKey || getEnv('HOLOSPHERE_PRIVATE_KEY'),
100
- logLevel: config.logLevel || getEnv('HOLOSPHERE_LOG_LEVEL') || 'WARN',
100
+ logLevel: config.logLevel || getEnv('HOLOSPHERE_LOG_LEVEL') || 'INFO',
101
101
  hybridMode: config.hybridMode !== false, // Enable by default
102
102
  };
103
103
 
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Nostr Utility Functions
3
3
  *
4
- * Provides browser-compatible utilities for Nostr key handling and NIP-04 encryption.
4
+ * Provides browser-compatible utilities for Nostr key handling and encryption.
5
+ * Supports both NIP-04 (legacy) and NIP-44 (modern, audited) encryption.
5
6
  * Apps can use these without directly importing nostr-tools.
6
7
  */
7
8
 
8
- import { nip04, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent } from 'nostr-tools';
9
+ import { nip04, nip44, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent } from 'nostr-tools';
9
10
 
10
11
  // ============================================================================
11
12
  // Key Conversion Utilities
@@ -160,6 +161,39 @@ export async function decryptNIP04(privateKey, senderPubKey, encryptedContent) {
160
161
  return await nip04.decrypt(privateKey, senderPubKey, encryptedContent);
161
162
  }
162
163
 
164
+ // ============================================================================
165
+ // NIP-44 Encryption (Modern, Audited - Replaces NIP-04)
166
+ // ============================================================================
167
+
168
+ /**
169
+ * Encrypt a message using NIP-44 (modern encryption standard)
170
+ * Uses ChaCha20 + HMAC-SHA256, audited by Cure53
171
+ * @param {string} privateKey - Sender's hex private key
172
+ * @param {string} recipientPubKey - Recipient's hex public key
173
+ * @param {string} content - Plain text content
174
+ * @returns {string} Encrypted content (base64)
175
+ */
176
+ export function encryptNIP44(privateKey, recipientPubKey, content) {
177
+ const privKeyBytes = hexToBytes(privateKey);
178
+ // nostr-tools nip44 expects pubkey as hex string, not bytes
179
+ const conversationKey = nip44.v2.utils.getConversationKey(privKeyBytes, recipientPubKey);
180
+ return nip44.v2.encrypt(content, conversationKey);
181
+ }
182
+
183
+ /**
184
+ * Decrypt a NIP-44 encrypted message
185
+ * @param {string} privateKey - Recipient's hex private key
186
+ * @param {string} senderPubKey - Sender's hex public key
187
+ * @param {string} encryptedContent - Encrypted content (base64)
188
+ * @returns {string} Decrypted content
189
+ */
190
+ export function decryptNIP44(privateKey, senderPubKey, encryptedContent) {
191
+ const privKeyBytes = hexToBytes(privateKey);
192
+ // nostr-tools nip44 expects pubkey as hex string, not bytes
193
+ const conversationKey = nip44.v2.utils.getConversationKey(privKeyBytes, senderPubKey);
194
+ return nip44.v2.decrypt(encryptedContent, conversationKey);
195
+ }
196
+
163
197
  // ============================================================================
164
198
  // Event Creation
165
199
  // ============================================================================
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Federation Handshake Protocol
3
3
  *
4
- * Uses NIP-04 encrypted DMs (kind 4) for bidirectional federation request/response.
4
+ * Uses NIP-44 encrypted DMs (kind 4) for bidirectional federation request/response.
5
+ * Falls back to NIP-04 for backward compatibility when receiving messages.
5
6
  * When user A federates with user B's pubkey, a DM is sent to B.
6
7
  * B can accept/reject, creating a matching federation on their side.
7
8
  */
8
9
 
9
10
  import {
11
+ encryptNIP44,
12
+ decryptNIP44,
10
13
  encryptNIP04,
11
14
  decryptNIP04,
12
15
  createDMEvent,
@@ -140,7 +143,7 @@ export function createFederationResponse({
140
143
  export async function sendFederationRequest(client, privateKey, recipientPubKey, request) {
141
144
  try {
142
145
  const content = JSON.stringify(request);
143
- const encrypted = await encryptNIP04(privateKey, recipientPubKey, content);
146
+ const encrypted = encryptNIP44(privateKey, recipientPubKey, content);
144
147
  const event = createDMEvent(recipientPubKey, encrypted, privateKey);
145
148
 
146
149
  if (client?.publish) {
@@ -168,7 +171,7 @@ export async function sendFederationRequest(client, privateKey, recipientPubKey,
168
171
  export async function sendFederationResponse(client, privateKey, recipientPubKey, response) {
169
172
  try {
170
173
  const content = JSON.stringify(response);
171
- const encrypted = await encryptNIP04(privateKey, recipientPubKey, content);
174
+ const encrypted = encryptNIP44(privateKey, recipientPubKey, content);
172
175
  const event = createDMEvent(recipientPubKey, encrypted, privateKey);
173
176
 
174
177
  if (client?.publish) {
@@ -218,7 +221,16 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
218
221
  if (!pTag || pTag[1] !== publicKey) return;
219
222
 
220
223
  try {
221
- const decrypted = await decryptNIP04(privateKey, event.pubkey, event.content);
224
+ let decrypted;
225
+
226
+ // Try NIP-44 first (modern encryption)
227
+ try {
228
+ decrypted = decryptNIP44(privateKey, event.pubkey, event.content);
229
+ } catch (nip44Error) {
230
+ // Fall back to NIP-04 for backward compatibility with older clients
231
+ decrypted = await decryptNIP04(privateKey, event.pubkey, event.content);
232
+ }
233
+
222
234
  const payload = JSON.parse(decrypted);
223
235
 
224
236
  if (payload.type === 'federation_request' && payload.version === '1.0') {
package/src/index.js CHANGED
@@ -170,6 +170,7 @@ class HoloSphereBase extends HoloSphereCore {
170
170
  }
171
171
 
172
172
  const path = storage.buildPath(this.config.appName, holonId, lensName, data.id);
173
+ this._log('DEBUG', 'write', { holonId, lensName, dataId: data.id, path });
173
174
  const existingData = await storage.read(this.client, path);
174
175
 
175
176
  // Handle hologram writes
@@ -286,10 +287,17 @@ class HoloSphereBase extends HoloSphereCore {
286
287
  data.target.lensName,
287
288
  data.target.dataId
288
289
  );
290
+ this._log('DEBUG', 'resolving hologram', {
291
+ hologramId: data.id,
292
+ sourcePath,
293
+ targetHolon: data.target.holonId,
294
+ targetLens: data.target.lensName,
295
+ targetDataId: data.target.dataId
296
+ });
289
297
 
290
298
  // Circular reference detection
291
299
  if (visited.has(sourcePath)) {
292
- console.warn(`Circular hologram reference detected: ${sourcePath}`);
300
+ this._log('WARN', 'Circular hologram reference detected', { sourcePath });
293
301
  return null;
294
302
  }
295
303
  visited.add(sourcePath);
@@ -301,6 +309,7 @@ class HoloSphereBase extends HoloSphereCore {
301
309
  }
302
310
 
303
311
  const sourceData = await storage.read(this.client, sourcePath, resolveOptions);
312
+ this._log('DEBUG', 'hologram source fetched', { found: !!sourceData, sourcePath });
304
313
  if (sourceData) {
305
314
  // If source is also a hologram, recursively resolve it
306
315
  let resolvedSource = sourceData;
@@ -369,14 +378,19 @@ class HoloSphereBase extends HoloSphereCore {
369
378
 
370
379
  if (dataId) {
371
380
  const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
381
+ this._log('DEBUG', 'read', { holonId, lensName, dataId, path });
372
382
  result = await storage.read(this.client, path);
383
+ this._log('DEBUG', 'read result', { found: !!result, isHologram: result?.hologram === true });
373
384
  } else {
374
385
  const path = storage.buildPath(this.config.appName, holonId, lensName);
386
+ this._log('DEBUG', 'readAll', { holonId, lensName, path });
375
387
  result = await storage.readAll(this.client, path);
388
+ this._log('DEBUG', 'readAll result', { count: Array.isArray(result) ? result.length : 0 });
376
389
  }
377
390
 
378
391
  const { resolveHolograms = true } = options;
379
- if (resolveHolograms) {
392
+ if (resolveHolograms && result) {
393
+ this._log('DEBUG', 'resolving holograms', { itemCount: Array.isArray(result) ? result.length : 1 });
380
394
  result = await this._resolveHolograms(result);
381
395
  }
382
396
 
@@ -25,28 +25,61 @@ export class GunDBBackend extends StorageBackend {
25
25
  // Subscription tracking
26
26
  this.subscriptions = new Map();
27
27
  this.subscriptionCounter = 0;
28
+
29
+ // Write cache for immediate consistency
30
+ // Gun's readAll/map().once() doesn't immediately see new writes - this cache bridges that gap
31
+ this.writeCache = new Map(); // path -> Map(id -> {data, timestamp})
32
+ this.writeCacheTTL = 300000; // Cache entries expire after 5 minutes (increased from 30s)
33
+
34
+ // Pending writes queue for retry
35
+ this.pendingWrites = new Map(); // path -> {data, retries, lastAttempt}
36
+ this.maxWriteRetries = 5;
37
+ this.writeRetryInterval = 10000; // 10 seconds between retries
28
38
  }
29
39
 
30
40
  async init() {
31
41
  // Dynamically import Gun to avoid issues if not installed
32
42
  let Gun;
33
43
  try {
44
+ console.log('[gundb-backend] Importing Gun...');
34
45
  const gunModule = await import('gun');
35
46
  Gun = gunModule.default || gunModule;
47
+ console.log('[gundb-backend] Gun imported:', typeof Gun);
36
48
  } catch (error) {
37
49
  throw new Error(
38
50
  'GunDB backend requires the "gun" package. Install it with: npm install gun'
39
51
  );
40
52
  }
41
53
 
54
+ // In Node.js, Gun needs a file path for radisk persistence
55
+ // Default to 'radata' if not specified
56
+ const dataDir = this.config.dataDir || 'radata';
57
+
42
58
  const gunConfig = {
43
59
  peers: this.config.peers || [],
44
60
  radisk: this.config.radisk !== false,
45
61
  localStorage: this.config.localStorage !== false,
46
- file: this.config.dataDir,
62
+ file: dataDir,
47
63
  };
48
64
 
65
+ console.log('[gundb-backend] Gun config:', JSON.stringify(gunConfig));
66
+
49
67
  this.gun = Gun(gunConfig);
68
+ console.log('[gundb-backend] Gun instance created:', !!this.gun);
69
+
70
+ // Test basic Gun functionality
71
+ try {
72
+ const testKey = `_test_${Date.now()}`;
73
+ this.gun.get(testKey).put({ test: true }, (ack) => {
74
+ console.log('[gundb-backend] Gun test write ack:', ack);
75
+ });
76
+ console.log('[gundb-backend] Gun test write initiated');
77
+ } catch (e) {
78
+ console.error('[gundb-backend] Gun test write failed:', e.message);
79
+ }
80
+
81
+ // Add a small delay to let Gun initialize
82
+ await new Promise(resolve => setTimeout(resolve, 500));
50
83
 
51
84
  // Generate or use provided key pair using Gun's SEA
52
85
  try {
@@ -82,6 +115,111 @@ export class GunDBBackend extends StorageBackend {
82
115
  await this.schemaValidator.init();
83
116
  }
84
117
 
118
+ // ============================================================================
119
+ // WRITE CACHE (for immediate consistency)
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Add item to the local write cache for immediate consistency
124
+ * @private
125
+ */
126
+ _addToWriteCache(path, data) {
127
+ if (!data || !data.id) return;
128
+
129
+ // Extract the parent path (without the data id)
130
+ const parentPath = path.substring(0, path.lastIndexOf('/'));
131
+ if (!parentPath) return;
132
+
133
+ if (!this.writeCache.has(parentPath)) {
134
+ this.writeCache.set(parentPath, new Map());
135
+ }
136
+
137
+ const pathCache = this.writeCache.get(parentPath);
138
+ pathCache.set(data.id.toString(), {
139
+ data: data,
140
+ timestamp: Date.now()
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Remove item from write cache (e.g., on delete)
146
+ * @private
147
+ */
148
+ _removeFromWriteCache(path, id) {
149
+ // Extract the parent path
150
+ const parentPath = path.substring(0, path.lastIndexOf('/'));
151
+ const pathCache = this.writeCache.get(parentPath);
152
+ if (pathCache) {
153
+ pathCache.delete(id.toString());
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get cached writes for a path, filtering out expired entries
159
+ * @private
160
+ */
161
+ _getWriteCacheEntries(path) {
162
+ const pathCache = this.writeCache.get(path);
163
+ if (!pathCache) return [];
164
+
165
+ const now = Date.now();
166
+ const validEntries = [];
167
+
168
+ for (const [id, entry] of pathCache) {
169
+ if (now - entry.timestamp < this.writeCacheTTL) {
170
+ validEntries.push(entry.data);
171
+ } else {
172
+ // Clean up expired entries
173
+ pathCache.delete(id);
174
+ }
175
+ }
176
+
177
+ return validEntries;
178
+ }
179
+
180
+ /**
181
+ * Merge cached writes with results from Gun
182
+ * @private
183
+ */
184
+ _mergeWithWriteCache(path, gunResults) {
185
+ const cachedWrites = this._getWriteCacheEntries(path);
186
+ if (cachedWrites.length === 0) return gunResults;
187
+
188
+ // Create a map by ID for deduplication
189
+ const resultMap = new Map();
190
+
191
+ // Add Gun results first
192
+ for (const item of gunResults) {
193
+ if (item && item.id) {
194
+ resultMap.set(item.id.toString(), item);
195
+ }
196
+ }
197
+
198
+ // Overlay cached writes (they're more recent)
199
+ for (const item of cachedWrites) {
200
+ if (item && item.id && !item._deleted) {
201
+ resultMap.set(item.id.toString(), item);
202
+ } else if (item && item.id && item._deleted) {
203
+ // Remove deleted items
204
+ resultMap.delete(item.id.toString());
205
+ }
206
+ }
207
+
208
+ return Array.from(resultMap.values());
209
+ }
210
+
211
+ /**
212
+ * Clear write cache for a specific path or all paths
213
+ * @param {string} [path] - Optional path to clear, clears all if not provided
214
+ */
215
+ clearWriteCache(path = null) {
216
+ if (path) {
217
+ this.writeCache.delete(path);
218
+ } else {
219
+ this.writeCache.clear();
220
+ }
221
+ }
222
+
85
223
  // ============================================================================
86
224
  // PATH BUILDING
87
225
  // ============================================================================
@@ -116,7 +254,59 @@ export class GunDBBackend extends StorageBackend {
116
254
  }
117
255
  }
118
256
 
119
- return wrapper.write(this.gun, path, data);
257
+ // Add to write cache FIRST for immediate consistency
258
+ // This ensures readAll returns the new data even if Gun write is slow/times out
259
+ this._addToWriteCache(path, data);
260
+
261
+ // Try to write to Gun with retry on timeout
262
+ const result = await this._writeWithRetry(path, data);
263
+
264
+ return result;
265
+ }
266
+
267
+ /**
268
+ * Write to Gun with automatic retry on timeout
269
+ * @private
270
+ */
271
+ async _writeWithRetry(path, data, attempt = 0) {
272
+ const result = await wrapper.write(this.gun, path, data);
273
+
274
+ if (result.timeout && attempt < this.maxWriteRetries) {
275
+ // Queue for background retry
276
+ this.pendingWrites.set(path, {
277
+ data,
278
+ retries: attempt + 1,
279
+ lastAttempt: Date.now()
280
+ });
281
+
282
+ // Schedule retry
283
+ this._scheduleRetry(path);
284
+ } else if (!result.timeout) {
285
+ // Success - remove from pending
286
+ this.pendingWrites.delete(path);
287
+ }
288
+
289
+ return result;
290
+ }
291
+
292
+ /**
293
+ * Schedule a background retry for a pending write
294
+ * @private
295
+ */
296
+ _scheduleRetry(path) {
297
+ setTimeout(async () => {
298
+ const pending = this.pendingWrites.get(path);
299
+ if (!pending) return;
300
+
301
+ if (pending.retries >= this.maxWriteRetries) {
302
+ console.warn(`[gundb-backend] Max retries reached for: ${path}`);
303
+ this.pendingWrites.delete(path);
304
+ return;
305
+ }
306
+
307
+ console.log(`[gundb-backend] Retrying write (attempt ${pending.retries + 1}): ${path}`);
308
+ await this._writeWithRetry(path, pending.data, pending.retries);
309
+ }, this.writeRetryInterval);
120
310
  }
121
311
 
122
312
  async read(path, options = {}) {
@@ -131,7 +321,10 @@ export class GunDBBackend extends StorageBackend {
131
321
  }
132
322
 
133
323
  async readAll(path, options = {}) {
134
- const items = await wrapper.readAll(this.gun, path);
324
+ const gunItems = await wrapper.readAll(this.gun, path);
325
+
326
+ // Merge with write cache for immediate consistency
327
+ const items = this._mergeWithWriteCache(path, gunItems);
135
328
 
136
329
  // Resolve references if requested
137
330
  if (options.resolveReferences) {
@@ -157,7 +350,24 @@ export class GunDBBackend extends StorageBackend {
157
350
  }
158
351
 
159
352
  async delete(path) {
160
- return wrapper.deleteData(this.gun, path);
353
+ const result = await wrapper.deleteData(this.gun, path);
354
+
355
+ // Extract id from path and mark as deleted in cache
356
+ const pathParts = path.split('/');
357
+ const id = pathParts[pathParts.length - 1];
358
+ if (id) {
359
+ // Add tombstone to cache so readAll doesn't return this item
360
+ const parentPath = path.substring(0, path.lastIndexOf('/'));
361
+ if (!this.writeCache.has(parentPath)) {
362
+ this.writeCache.set(parentPath, new Map());
363
+ }
364
+ this.writeCache.get(parentPath).set(id.toString(), {
365
+ data: { id, _deleted: true },
366
+ timestamp: Date.now()
367
+ });
368
+ }
369
+
370
+ return result;
161
371
  }
162
372
 
163
373
  async deleteAll(path) {
@@ -180,7 +390,62 @@ export class GunDBBackend extends StorageBackend {
180
390
  // ============================================================================
181
391
 
182
392
  async writeGlobal(tableName, data) {
183
- return wrapper.writeGlobal(this.gun, this.appName, tableName, data);
393
+ // Add to write cache FIRST for immediate consistency
394
+ // This ensures readAllGlobal returns the new data even if Gun write is slow/times out
395
+ const path = this.buildGlobalPath(tableName, data.id);
396
+ this._addToWriteCache(path, data);
397
+
398
+ // Try to write to Gun with retry on timeout
399
+ const result = await this._writeGlobalWithRetry(tableName, data);
400
+
401
+ return result;
402
+ }
403
+
404
+ /**
405
+ * Write to global table with automatic retry on timeout
406
+ * @private
407
+ */
408
+ async _writeGlobalWithRetry(tableName, data, attempt = 0) {
409
+ const path = this.buildGlobalPath(tableName, data.id);
410
+ const result = await wrapper.writeGlobal(this.gun, this.appName, tableName, data);
411
+
412
+ if (result.timeout && attempt < this.maxWriteRetries) {
413
+ // Queue for background retry
414
+ this.pendingWrites.set(path, {
415
+ data: { tableName, data },
416
+ isGlobal: true,
417
+ retries: attempt + 1,
418
+ lastAttempt: Date.now()
419
+ });
420
+
421
+ // Schedule retry
422
+ this._scheduleGlobalRetry(path, tableName);
423
+ } else if (!result.timeout) {
424
+ // Success - remove from pending
425
+ this.pendingWrites.delete(path);
426
+ }
427
+
428
+ return result;
429
+ }
430
+
431
+ /**
432
+ * Schedule a background retry for a pending global write
433
+ * @private
434
+ */
435
+ _scheduleGlobalRetry(path, tableName) {
436
+ setTimeout(async () => {
437
+ const pending = this.pendingWrites.get(path);
438
+ if (!pending || !pending.isGlobal) return;
439
+
440
+ if (pending.retries >= this.maxWriteRetries) {
441
+ console.warn(`[gundb-backend] Max retries reached for global: ${path}`);
442
+ this.pendingWrites.delete(path);
443
+ return;
444
+ }
445
+
446
+ console.log(`[gundb-backend] Retrying global write (attempt ${pending.retries + 1}): ${path}`);
447
+ await this._writeGlobalWithRetry(tableName, pending.data.data, pending.retries);
448
+ }, this.writeRetryInterval);
184
449
  }
185
450
 
186
451
  async readGlobal(tableName, key) {
@@ -188,11 +453,27 @@ export class GunDBBackend extends StorageBackend {
188
453
  }
189
454
 
190
455
  async readAllGlobal(tableName, timeout = 5000) {
191
- return wrapper.readAllGlobal(this.gun, this.appName, tableName, timeout);
456
+ const gunItems = await wrapper.readAllGlobal(this.gun, this.appName, tableName, timeout);
457
+
458
+ // Merge with write cache for immediate consistency
459
+ const path = this.buildGlobalPath(tableName);
460
+ return this._mergeWithWriteCache(path, gunItems);
192
461
  }
193
462
 
194
463
  async deleteGlobal(tableName, key) {
195
- return wrapper.deleteGlobal(this.gun, this.appName, tableName, key);
464
+ const result = await wrapper.deleteGlobal(this.gun, this.appName, tableName, key);
465
+
466
+ // Mark as deleted in cache
467
+ const path = this.buildGlobalPath(tableName);
468
+ if (!this.writeCache.has(path)) {
469
+ this.writeCache.set(path, new Map());
470
+ }
471
+ this.writeCache.get(path).set(key.toString(), {
472
+ data: { id: key, _deleted: true },
473
+ timestamp: Date.now()
474
+ });
475
+
476
+ return result;
196
477
  }
197
478
 
198
479
  async deleteAllGlobal(tableName) {
@@ -517,12 +798,15 @@ export class GunDBBackend extends StorageBackend {
517
798
  this.schemaValidator.clearCache();
518
799
  }
519
800
 
520
- // 3. Logout auth
801
+ // 3. Clear write cache
802
+ this.writeCache.clear();
803
+
804
+ // 4. Logout auth
521
805
  if (this.auth) {
522
806
  this.auth.logout();
523
807
  }
524
808
 
525
- // 4. Clean up Gun connections
809
+ // 5. Clean up Gun connections
526
810
  if (this.gun) {
527
811
  try {
528
812
  // Clean up mesh connections