holosphere 2.0.0-alpha6 → 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 (42) hide show
  1. package/dist/cjs/holosphere.cjs +1 -1
  2. package/dist/esm/holosphere.js +1 -1
  3. package/dist/{index-NOravBLu.js → index-4XHHKe6S.js} +383 -102
  4. package/dist/index-4XHHKe6S.js.map +1 -0
  5. package/dist/{index-JFz-dW43.js → index-BjP1TXGz.js} +2 -2
  6. package/dist/{index-JFz-dW43.js.map → index-BjP1TXGz.js.map} +1 -1
  7. package/dist/{index-CmzkI7SI.cjs → index-CKffQDmQ.cjs} +2 -2
  8. package/dist/{index-CmzkI7SI.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-C4HsulhA.cjs → indexeddb-storage-DD7EFBVc.cjs} +2 -2
  12. package/dist/{indexeddb-storage-C4HsulhA.cjs.map → indexeddb-storage-DD7EFBVc.cjs.map} +1 -1
  13. package/dist/{indexeddb-storage-OtSAVDZY.js → indexeddb-storage-lExjjFlV.js} +2 -2
  14. package/dist/{indexeddb-storage-OtSAVDZY.js.map → indexeddb-storage-lExjjFlV.js.map} +1 -1
  15. package/dist/{memory-storage-ChpcYvxA.js → memory-storage-C68adso2.js} +2 -2
  16. package/dist/{memory-storage-ChpcYvxA.js.map → memory-storage-C68adso2.js.map} +1 -1
  17. package/dist/{memory-storage-MD6ED00P.cjs → memory-storage-DD_6yyXT.cjs} +2 -2
  18. package/dist/{memory-storage-MD6ED00P.cjs.map → memory-storage-DD_6yyXT.cjs.map} +1 -1
  19. package/dist/{secp256k1-DcTYQrqC.cjs → secp256k1-DYELiqgx.cjs} +2 -2
  20. package/dist/{secp256k1-DcTYQrqC.cjs.map → secp256k1-DYELiqgx.cjs.map} +1 -1
  21. package/dist/{secp256k1-PfNOEI7a.js → secp256k1-OM8siPyy.js} +2 -2
  22. package/dist/{secp256k1-PfNOEI7a.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-async.js +14 -12
  35. package/src/storage/gun-auth.js +26 -18
  36. package/src/storage/gun-wrapper.js +75 -41
  37. package/src/storage/nostr-async.js +40 -25
  38. package/src/storage/unified-storage.js +31 -1
  39. package/vite.config.cdn.js +60 -0
  40. package/dist/index-BtKHqqet.cjs +0 -5
  41. package/dist/index-BtKHqqet.cjs.map +0 -1
  42. package/dist/index-NOravBLu.js.map +0 -1
@@ -274,28 +274,36 @@ export class GunAuth {
274
274
  return;
275
275
  }
276
276
 
277
- // Count references that need fetching
278
- expectedCount = keys.filter(k => {
279
- const item = parentData[k];
280
- return item && typeof item === 'object' && item['#'];
281
- }).length;
282
-
283
- // If no references, we're done
284
- if (expectedCount === 0) {
285
- settled = true;
286
- resolve([]);
287
- return;
288
- }
277
+ // Pre-parse inline items (not Gun references)
278
+ for (const key of keys) {
279
+ const rawItem = parentData[key];
280
+ if (!rawItem) continue;
289
281
 
290
- // Step 2: Collect items, counting as we go
291
- ref.map().once((data, key) => {
292
- if (settled || !data || key.startsWith('_') || seen.has(key)) return;
293
- seen.add(key);
282
+ // Skip Gun references - will be fetched via map().once()
283
+ if (typeof rawItem === 'object' && rawItem['#']) continue;
294
284
 
295
- const parsed = parseItem(data);
296
- if (parsed && !parsed._deleted) {
285
+ const parsed = parseItem(rawItem);
286
+ if (parsed && !parsed._deleted && !seen.has(key)) {
287
+ seen.add(key);
297
288
  results.push(parsed);
298
289
  }
290
+ }
291
+
292
+ // Expected count is ALL keys (map().once() fires for all)
293
+ expectedCount = keys.length;
294
+
295
+ // Step 2: Collect items via map().once(), counting as we go
296
+ ref.map().once((data, key) => {
297
+ if (settled || !data || key.startsWith('_')) return;
298
+
299
+ // Count every item, but only add if not already seen
300
+ if (!seen.has(key)) {
301
+ seen.add(key);
302
+ const parsed = parseItem(data);
303
+ if (parsed && !parsed._deleted) {
304
+ results.push(parsed);
305
+ }
306
+ }
299
307
  receivedCount++;
300
308
  tryResolve();
301
309
  });
@@ -54,15 +54,32 @@ function encodePathComponent(component) {
54
54
  return encodeURIComponent(component).replace(/%2F/g, '/');
55
55
  }
56
56
 
57
+ /**
58
+ * Navigate to a Gun path using chained .get() calls
59
+ * Gun treats 'a/b/c' as a literal key, not a path.
60
+ * This function splits the path and chains .get() calls properly.
61
+ * @private
62
+ * @param {Object} gun - Gun instance
63
+ * @param {string} path - Path string like "appname/holon/lens/key"
64
+ * @returns {Object} Gun chain reference at the path
65
+ */
66
+ function getGunPath(gun, path) {
67
+ const parts = path.split('/').filter(p => p.length > 0);
68
+ let ref = gun;
69
+ for (const part of parts) {
70
+ ref = ref.get(part);
71
+ }
72
+ return ref;
73
+ }
74
+
57
75
  /**
58
76
  * Serialize data for GunDB storage
59
- * Wraps data in { _json: string } format for Gun compatibility
60
- * Gun requires an object at graph roots, so we can't store raw JSON strings
61
- * The deserialization handles both this format and raw JSON strings for reading old data
77
+ * Stores data as raw JSON string for compatibility with holosphere original
78
+ * This matches the format used in holosphere v1 for better interoperability
62
79
  * @private
63
80
  */
64
81
  function serializeForGun(data) {
65
- return { _json: JSON.stringify(data) };
82
+ return JSON.stringify(data);
66
83
  }
67
84
 
68
85
  /**
@@ -152,11 +169,21 @@ function deserializeFromGun(data) {
152
169
  export async function write(gun, path, data) {
153
170
  try {
154
171
  const serialized = serializeForGun(data);
155
- await gunPut(gun.get(path), serialized, 2000);
156
- // Delay to allow Gun to propagate the write (50ms for better reliability)
157
- await new Promise(resolve => setTimeout(resolve, 50));
158
- return true;
172
+ const parts = path.split('/').filter(p => p.length > 0);
173
+ console.log('[gun-wrapper] write:', { path, parts, dataId: data?.id });
174
+ const ref = getGunPath(gun, path);
175
+ console.log('[gun-wrapper] write ref soul:', ref?._.get);
176
+ const ack = await gunPut(ref, serialized, 5000); // Increased timeout from 2s to 5s
177
+ console.log('[gun-wrapper] write ack:', { ok: ack.ok, timeout: ack.timeout });
178
+ if (ack.timeout) {
179
+ console.warn('[gun-wrapper] write timed out (data may not be persisted):', path);
180
+ }
181
+ console.log('[gun-wrapper] write complete:', path);
182
+
183
+ // Return ack info so caller can handle timeouts
184
+ return { ok: true, timeout: ack.timeout || false };
159
185
  } catch (error) {
186
+ console.error('[gun-wrapper] write error:', error);
160
187
  throw error;
161
188
  }
162
189
  }
@@ -168,7 +195,12 @@ export async function write(gun, path, data) {
168
195
  * @returns {Promise<Object|null>} Data or null if not found
169
196
  */
170
197
  export async function read(gun, path) {
171
- const rawData = await gunPromise(gun.get(path), 2000);
198
+ const parts = path.split('/').filter(p => p.length > 0);
199
+ console.log('[gun-wrapper] read:', { path, parts });
200
+ const ref = getGunPath(gun, path);
201
+ console.log('[gun-wrapper] read ref soul:', ref?._.get);
202
+ const rawData = await gunPromise(ref, 2000);
203
+ console.log('[gun-wrapper] read rawData:', rawData ? (typeof rawData === 'string' ? rawData.substring(0, 100) : 'object') : 'null');
172
204
 
173
205
  if (!rawData) {
174
206
  return null;
@@ -193,53 +225,52 @@ export async function read(gun, path) {
193
225
  * @returns {Promise<Object[]>} Array of data objects
194
226
  */
195
227
  export async function readAll(gun, path, timeout = 5000) {
228
+ const parts = path.split('/').filter(p => p.length > 0);
229
+ console.log('[gun-wrapper] readAll:', { path, parts });
230
+
196
231
  return new Promise((resolve) => {
197
232
  const output = new Map();
198
233
  let settled = false;
199
234
  let expectedCount = 0;
200
235
  let receivedCount = 0;
201
236
 
202
- const ref = gun.get(path);
237
+ const ref = getGunPath(gun, path);
238
+ console.log('[gun-wrapper] readAll ref soul:', ref?._.get);
203
239
 
204
240
  const tryResolve = () => {
205
241
  if (settled) return;
206
242
  if (expectedCount > 0 && receivedCount >= expectedCount) {
207
243
  settled = true;
244
+ console.log('[gun-wrapper] readAll resolved with', output.size, 'items');
208
245
  resolve(Array.from(output.values()));
209
246
  }
210
247
  };
211
248
 
212
249
  const parseItem = (data) => {
213
- if (!data) return null;
214
-
215
- let item = null;
216
- if (data._json && typeof data._json === 'string') {
217
- try {
218
- item = JSON.parse(data._json);
219
- } catch (e) {}
220
- } else if (typeof data === 'string') {
221
- try {
222
- item = JSON.parse(data);
223
- } catch (e) {}
224
- }
225
- return item;
250
+ return deserializeFromGun(data);
226
251
  };
227
252
 
228
253
  // Step 1: Get the parent data to count expected items
229
254
  ref.once((parentData) => {
230
255
  if (settled) return;
256
+ console.log('[gun-wrapper] readAll parentData:', parentData);
257
+ console.log('[gun-wrapper] readAll parentData keys:', parentData ? Object.keys(parentData).filter(k => k !== '_') : 'null');
258
+ console.log('[gun-wrapper] readAll parentData type:', typeof parentData);
231
259
 
232
260
  if (!parentData) {
233
261
  settled = true;
262
+ console.log('[gun-wrapper] readAll: no parent data, returning empty');
234
263
  resolve([]);
235
264
  return;
236
265
  }
237
266
 
238
267
  // Get all keys except Gun metadata
239
268
  const keys = Object.keys(parentData).filter(k => k !== '_');
269
+ console.log('[gun-wrapper] readAll keys:', keys);
240
270
 
241
271
  if (keys.length === 0) {
242
272
  settled = true;
273
+ console.log('[gun-wrapper] readAll: no keys, returning empty');
243
274
  resolve([]);
244
275
  return;
245
276
  }
@@ -257,40 +288,38 @@ export async function readAll(gun, path, timeout = 5000) {
257
288
  continue;
258
289
  }
259
290
 
260
- // Try to parse inline data
291
+ // Try to parse inline data (don't count yet - will count in map().once() phase)
261
292
  const item = parseItem(rawItem);
262
293
  if (item && item.id && !item._deleted) {
263
294
  output.set(item.id, item);
264
- receivedCount++;
265
295
  }
266
296
  }
267
297
 
268
- // Set expected count: references that need fetching
269
- expectedCount = referenceKeys.length;
298
+ // Set expected count: ALL keys that could have data (references + inline)
299
+ // We use total keys because map().once() will fire for all of them
300
+ expectedCount = keys.length;
270
301
 
271
- // If no references to resolve, we're done
302
+ // If no keys, we're done (shouldn't happen but be safe)
272
303
  if (expectedCount === 0) {
273
304
  settled = true;
274
305
  resolve(Array.from(output.values()));
275
306
  return;
276
307
  }
277
308
 
278
- // Step 2: Use map().once() to resolve references, counting as we go
309
+ // Step 2: Use map().once() to resolve all items, counting as we go
279
310
  ref.map().once((data, key) => {
280
311
  if (settled || !data || key === '_') return;
281
312
 
282
313
  const item = parseItem(data);
283
314
  if (item && item.id && !item._deleted) {
315
+ // Add to output if not already there (inline items already added)
284
316
  if (!output.has(item.id)) {
285
317
  output.set(item.id, item);
286
- receivedCount++;
287
- tryResolve();
288
318
  }
289
- } else {
290
- // Item was null/deleted, still count it as received
291
- receivedCount++;
292
- tryResolve();
293
319
  }
320
+ // Count every item received (inline or reference)
321
+ receivedCount++;
322
+ tryResolve();
294
323
  });
295
324
  });
296
325
 
@@ -312,7 +341,7 @@ export async function readAll(gun, path, timeout = 5000) {
312
341
  * @returns {Promise<boolean>} Success indicator
313
342
  */
314
343
  export async function update(gun, path, updates) {
315
- const rawData = await gunPromise(gun.get(path));
344
+ const rawData = await gunPromise(getGunPath(gun, path));
316
345
 
317
346
  if (!rawData) {
318
347
  return false; // Not found
@@ -329,7 +358,9 @@ export async function update(gun, path, updates) {
329
358
 
330
359
  try {
331
360
  const serialized = serializeForGun(merged);
332
- await gunPut(gun.get(path), serialized);
361
+ await gunPut(getGunPath(gun, path), serialized, 2000);
362
+ // Add delay for propagation
363
+ await new Promise(resolve => setTimeout(resolve, 200));
333
364
  return true;
334
365
  } catch (error) {
335
366
  throw error;
@@ -345,7 +376,7 @@ export async function update(gun, path, updates) {
345
376
  export async function deleteData(gun, path) {
346
377
  try {
347
378
  // First read existing data to get the id
348
- const rawData = await gunPromise(gun.get(path));
379
+ const rawData = await gunPromise(getGunPath(gun, path));
349
380
  if (!rawData) {
350
381
  return true; // Already deleted/doesn't exist
351
382
  }
@@ -359,7 +390,9 @@ export async function deleteData(gun, path) {
359
390
  _deletedAt: Date.now()
360
391
  };
361
392
 
362
- await gunPut(gun.get(path), serializeForGun(tombstone));
393
+ await gunPut(getGunPath(gun, path), serializeForGun(tombstone), 2000);
394
+ // Add delay for propagation
395
+ await new Promise(resolve => setTimeout(resolve, 200));
363
396
  return true;
364
397
  } catch (error) {
365
398
  throw error;
@@ -403,7 +436,7 @@ export function subscribe(gun, path, callback, options = {}) {
403
436
 
404
437
  if (isPrefix) {
405
438
  // Subscribe to all items under this prefix
406
- const ref = gun.get(path);
439
+ const ref = getGunPath(gun, path);
407
440
 
408
441
  ref.map().on((data, key) => {
409
442
  if (data && !key.startsWith('_')) {
@@ -425,7 +458,7 @@ export function subscribe(gun, path, callback, options = {}) {
425
458
  };
426
459
  } else {
427
460
  // Subscribe to single item
428
- const listener = gun.get(path).on((data, key) => {
461
+ const listener = getGunPath(gun, path).on((data, key) => {
429
462
  if (data) {
430
463
  const deserialized = deserializeFromGun(data);
431
464
  if (deserialized && !deserialized._deleted) {
@@ -464,6 +497,7 @@ export async function writeGlobal(gun, appname, tableName, data) {
464
497
  throw new Error('writeGlobal: data must have an id field');
465
498
  }
466
499
  const path = buildGlobalPath(appname, tableName, data.id);
500
+ // Use write function which includes the propagation delay
467
501
  return write(gun, path, data);
468
502
  }
469
503
 
@@ -65,28 +65,33 @@ export async function nostrGet(client, path, kind = 30000, options = {}) {
65
65
  if (!options.skipPersistent && client.persistentGet) {
66
66
  const persistedEvent = await client.persistentGet(path);
67
67
  if (persistedEvent && persistedEvent.content) {
68
- try {
69
- const data = JSON.parse(persistedEvent.content);
68
+ // Verify author is in requested authors list (persistent storage may have cached events from other authors)
69
+ if (!authors.includes(persistedEvent.pubkey)) {
70
+ // Author mismatch - fall through to relay query
71
+ } else {
72
+ try {
73
+ const data = JSON.parse(persistedEvent.content);
70
74
 
71
- // Skip deleted items
72
- if (data._deleted) {
73
- return null;
74
- }
75
+ // Skip deleted items
76
+ if (data._deleted) {
77
+ return null;
78
+ }
75
79
 
76
- // Optionally include author information
77
- if (options.includeAuthor) {
78
- data._author = persistedEvent.pubkey;
79
- }
80
+ // Optionally include author information
81
+ if (options.includeAuthor) {
82
+ data._author = persistedEvent.pubkey;
83
+ }
80
84
 
81
- // Trigger background refresh from relays (fire-and-forget)
82
- if (client.refreshPathInBackground) {
83
- client.refreshPathInBackground(path, kind, { authors, timeout });
84
- }
85
+ // Trigger background refresh from relays (fire-and-forget)
86
+ if (client.refreshPathInBackground) {
87
+ client.refreshPathInBackground(path, kind, { authors, timeout });
88
+ }
85
89
 
86
- return data;
87
- } catch (error) {
88
- // Fall through to relay query if parsing fails
89
- console.warn('[nostrGet] Failed to parse persisted event:', error);
90
+ return data;
91
+ } catch (error) {
92
+ // Fall through to relay query if parsing fails
93
+ console.warn('[nostrGet] Failed to parse persisted event:', error);
94
+ }
90
95
  }
91
96
  }
92
97
  }
@@ -136,12 +141,15 @@ async function _executeNostrGet(client, path, kind, authors, timeout, options) {
136
141
 
137
142
  const events = await client.query(filter, { timeout });
138
143
 
139
- if (events.length === 0) {
144
+ // Filter by author (relays may not respect authors filter)
145
+ const authoredEvents = events.filter(event => authors.includes(event.pubkey));
146
+
147
+ if (authoredEvents.length === 0) {
140
148
  return null;
141
149
  }
142
150
 
143
- // Get most recent event (across all authors)
144
- const event = events.sort((a, b) => b.created_at - a.created_at)[0];
151
+ // Get most recent event (across allowed authors)
152
+ const event = authoredEvents.sort((a, b) => b.created_at - a.created_at)[0];
145
153
 
146
154
  try {
147
155
  const data = JSON.parse(event.content);
@@ -187,6 +195,9 @@ export async function nostrGetAll(client, pathPrefix, kind = 30000, options = {}
187
195
  for (const event of persistedEvents) {
188
196
  if (!event || !event.tags) continue;
189
197
 
198
+ // Verify author is in requested authors list (persistent storage may have cached events from other authors)
199
+ if (!authors.includes(event.pubkey)) continue;
200
+
190
201
  const dTag = event.tags.find(t => t[0] === 'd');
191
202
  if (!dTag || !dTag[1] || !dTag[1].startsWith(pathPrefix)) continue;
192
203
 
@@ -267,10 +278,12 @@ async function _executeNostrGetAll(client, pathPrefix, kind, authors, timeout, l
267
278
 
268
279
  const events = await client.query(filter, { timeout });
269
280
 
270
- // Filter by path prefix in application layer
281
+ // Filter by path prefix AND verify author (relays may not respect authors filter)
271
282
  const matching = events.filter(event => {
272
283
  const dTag = event.tags.find(t => t[0] === 'd');
273
- return dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
284
+ const pathMatches = dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
285
+ const authorAllowed = authors.includes(event.pubkey);
286
+ return pathMatches && authorAllowed;
274
287
  });
275
288
 
276
289
  // Parse content and group by d-tag (keep latest only, across all authors)
@@ -331,10 +344,12 @@ export async function nostrGetAllHybrid(client, pathPrefix, kind = 30000, option
331
344
 
332
345
  const events = await queryMethod.call(client, filter, { timeout });
333
346
 
334
- // Filter by path prefix in application layer
347
+ // Filter by path prefix AND verify author (relays may not respect authors filter)
335
348
  const matching = events.filter(event => {
336
349
  const dTag = event.tags.find(t => t[0] === 'd');
337
- return dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
350
+ const pathMatches = dTag && dTag[1] && dTag[1].startsWith(pathPrefix);
351
+ const authorAllowed = authors.includes(event.pubkey);
352
+ return pathMatches && authorAllowed;
338
353
  });
339
354
 
340
355
  // Parse content and group by d-tag (keep latest only)
@@ -27,7 +27,11 @@ export function buildPath(appName, holonId, lensName, key = null) {
27
27
  * @returns {Promise<boolean>} Success indicator
28
28
  */
29
29
  export async function write(client, path, data) {
30
- // Check if this is a GunDB client
30
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
31
+ if (client.write && client.gun) {
32
+ return client.write(path, data);
33
+ }
34
+ // Fallback to direct gunWrapper (no write cache)
31
35
  if (client.gun) {
32
36
  return gunWrapper.write(client.gun, path, data);
33
37
  }
@@ -43,6 +47,11 @@ export async function write(client, path, data) {
43
47
  * @returns {Promise<Object|null>} Data or null
44
48
  */
45
49
  export async function read(client, path, options = {}) {
50
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
51
+ if (client.read && client.gun) {
52
+ return client.read(path, options);
53
+ }
54
+ // Fallback to direct gunWrapper
46
55
  if (client.gun) {
47
56
  return gunWrapper.read(client.gun, path);
48
57
  }
@@ -57,6 +66,11 @@ export async function read(client, path, options = {}) {
57
66
  * @returns {Promise<Object[]>} Array of data objects
58
67
  */
59
68
  export async function readAll(client, path, options = {}) {
69
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
70
+ if (client.readAll && client.gun) {
71
+ return client.readAll(path, options);
72
+ }
73
+ // Fallback to direct gunWrapper
60
74
  if (client.gun) {
61
75
  return gunWrapper.readAll(client.gun, path);
62
76
  }
@@ -71,6 +85,11 @@ export async function readAll(client, path, options = {}) {
71
85
  * @returns {Promise<boolean>} Success indicator
72
86
  */
73
87
  export async function update(client, path, updates) {
88
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
89
+ if (client.update && client.gun) {
90
+ return client.update(path, updates);
91
+ }
92
+ // Fallback to direct gunWrapper
74
93
  if (client.gun) {
75
94
  return gunWrapper.update(client.gun, path, updates);
76
95
  }
@@ -84,6 +103,11 @@ export async function update(client, path, updates) {
84
103
  * @returns {Promise<boolean>} Success indicator
85
104
  */
86
105
  export async function deleteData(client, path) {
106
+ // Check if this is a GunDB client with backend methods (preferred - has write cache)
107
+ if (client.delete && client.gun) {
108
+ return client.delete(path);
109
+ }
110
+ // Fallback to direct gunWrapper
87
111
  if (client.gun) {
88
112
  return gunWrapper.deleteData(client.gun, path);
89
113
  }
@@ -97,6 +121,7 @@ export async function deleteData(client, path) {
97
121
  * @returns {Promise<Object>} Deletion results
98
122
  */
99
123
  export async function deleteAll(client, path) {
124
+ // Fallback to direct gunWrapper (deleteAll not typically in client interface)
100
125
  if (client.gun) {
101
126
  return gunWrapper.deleteAll(client.gun, path);
102
127
  }
@@ -112,6 +137,11 @@ export async function deleteAll(client, path) {
112
137
  * @returns {Object} Subscription with unsubscribe method
113
138
  */
114
139
  export function subscribe(client, path, callback, options = {}) {
140
+ // Check if this is a GunDB client with backend methods
141
+ if (client.subscribe && client.gun) {
142
+ return client.subscribe(path, callback, options);
143
+ }
144
+ // Fallback to direct gunWrapper
115
145
  if (client.gun) {
116
146
  return gunWrapper.subscribe(client.gun, path, callback, options);
117
147
  }
@@ -0,0 +1,60 @@
1
+ import { defineConfig } from 'vite';
2
+ import { resolve } from 'path';
3
+
4
+ /**
5
+ * CDN Build Configuration
6
+ * Creates a self-contained bundle for browser usage via CDN
7
+ * All dependencies are bundled (no externals)
8
+ */
9
+ export default defineConfig({
10
+ build: {
11
+ target: 'es2020',
12
+ outDir: 'dist/cdn',
13
+ lib: {
14
+ entry: resolve(__dirname, 'src/cdn-entry.js'),
15
+ name: 'HoloSphere',
16
+ formats: ['iife'],
17
+ fileName: () => 'holosphere.min.js',
18
+ },
19
+ rollupOptions: {
20
+ // Bundle all dependencies for CDN
21
+ external: [],
22
+ output: {
23
+ // Use default export so HoloSphere is directly accessible
24
+ exports: 'default',
25
+ // Extend window with all exports
26
+ extend: true,
27
+ // Provide globals for any remaining externals
28
+ globals: {},
29
+ // Inline dynamic imports
30
+ inlineDynamicImports: true,
31
+ },
32
+ },
33
+ sourcemap: true,
34
+ minify: 'terser',
35
+ terserOptions: {
36
+ compress: {
37
+ drop_console: false,
38
+ passes: 2,
39
+ },
40
+ mangle: {
41
+ safari10: true,
42
+ },
43
+ },
44
+ // Increase chunk size warning limit for CDN bundle
45
+ chunkSizeWarningLimit: 2000,
46
+ },
47
+ resolve: {
48
+ alias: [
49
+ {
50
+ find: './filesystem-storage.js',
51
+ replacement: resolve(__dirname, 'src/storage/filesystem-storage-browser.js'),
52
+ },
53
+ ],
54
+ },
55
+ define: {
56
+ 'process.versions.node': JSON.stringify(undefined),
57
+ 'process.env.NODE_ENV': JSON.stringify('production'),
58
+ 'global': 'globalThis',
59
+ },
60
+ });