holosphere 2.0.0-alpha16 → 2.0.0-alpha18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "holosphere",
3
- "version": "2.0.0-alpha16",
3
+ "version": "2.0.0-alpha18",
4
4
  "description": "Holonic geospatial communication infrastructure combining H3 hexagonal indexing with distributed P2P storage",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -875,21 +875,22 @@ class HoloSphereBase extends HoloSphereCore {
875
875
  }
876
876
 
877
877
  /**
878
- * Recursively resolves holograms (references) to their source data.
879
- * Handles circular reference detection and local overrides.
878
+ * Resolves holograms (references) to their source data.
879
+ * Iterates arrays and filters out invalid entries; delegates individual
880
+ * hologram resolution (including capability verification and recursive
881
+ * chain resolution) to federation.resolveHologram().
880
882
  *
881
883
  * @private
882
884
  * @param {Object|Array|null} data - Data that may contain holograms
883
- * @param {Set<string>} [visited=new Set()] - Set of visited paths for circular detection
884
885
  * @returns {Promise<Object|Array|null>} Resolved data with holograms replaced by source data
885
886
  */
886
- async _resolveHolograms(data, visited = new Set()) {
887
+ async _resolveHolograms(data) {
887
888
  if (!data) return data;
888
889
 
889
890
  if (Array.isArray(data)) {
890
891
  const resolved = [];
891
892
  for (const item of data) {
892
- const resolvedItem = await this._resolveHolograms(item, new Set());
893
+ const resolvedItem = await this._resolveHolograms(item);
893
894
  if (resolvedItem !== null) {
894
895
  resolved.push(resolvedItem);
895
896
  }
@@ -897,81 +898,13 @@ class HoloSphereBase extends HoloSphereCore {
897
898
  return resolved;
898
899
  }
899
900
 
900
- if (data && typeof data === 'object') {
901
- if (data.hologram === true && data.target) {
902
- const sourcePath = storage.buildPath(
903
- data.target.appname || this.config.appName,
904
- data.target.holonId,
905
- data.target.lensName,
906
- data.target.dataId
907
- );
908
- this._log('DEBUG', 'resolving hologram', {
909
- hologramId: data.id,
910
- sourcePath,
911
- targetHolon: data.target.holonId,
912
- targetLens: data.target.lensName,
913
- targetDataId: data.target.dataId
914
- });
915
-
916
- // Circular reference detection
917
- if (visited.has(sourcePath)) {
918
- this._log('WARN', 'Circular hologram reference detected', { sourcePath });
919
- return null;
920
- }
921
- visited.add(sourcePath);
922
-
923
- let resolveOptions = {};
924
- if (data.target.authorPubKey) {
925
- resolveOptions.authors = [data.target.authorPubKey];
926
- resolveOptions.includeAuthor = true;
927
- }
928
-
929
- const sourceData = await storage.read(this.client, sourcePath, resolveOptions);
930
- this._log('DEBUG', 'hologram source fetched', { found: !!sourceData, sourcePath });
931
- if (sourceData) {
932
- // If source is also a hologram, recursively resolve it
933
- let resolvedSource = sourceData;
934
- if (sourceData.hologram === true && sourceData.target) {
935
- resolvedSource = await this._resolveHolograms(sourceData, visited);
936
- if (resolvedSource === null) {
937
- return null; // Circular reference or unresolvable
938
- }
939
- }
940
-
941
- // Get local override fields from the hologram (excluding hologram structure fields)
942
- const hologramStructureFields = ['hologram', 'soul', 'target', '_meta', 'id', 'capability', 'crossHolosphere'];
943
- const localOverrides = {};
944
- for (const [k, v] of Object.entries(data)) {
945
- if (!hologramStructureFields.includes(k)) {
946
- localOverrides[k] = v;
947
- }
948
- }
949
-
950
- const merged = {
951
- ...resolvedSource,
952
- ...localOverrides,
953
- _hologram: {
954
- isHologram: true,
955
- soul: data.soul,
956
- sourceHolon: data.target.holonId,
957
- source: data.target,
958
- localOverrides: Object.keys(localOverrides),
959
- crossHolosphere: data.crossHolosphere || false,
960
- }
961
- };
962
-
963
- // Preserve source _meta but add hologram source info
964
- if (resolvedSource._meta) {
965
- merged._meta = { ...resolvedSource._meta, source: data.target.holonId };
966
- } else {
967
- merged._meta = { source: data.target.holonId };
968
- }
969
-
970
- return merged;
971
- }
972
- return null; // Source not found
973
- }
901
+ if (data?.hologram === true && data.target) {
902
+ return federation.resolveHologram(this.client, data, new Set(), [], {
903
+ appname: this.config.appName,
904
+ deleteCircular: false,
905
+ });
974
906
  }
907
+
975
908
  return data;
976
909
  }
977
910
 
@@ -1098,7 +1031,7 @@ class HoloSphereBase extends HoloSphereCore {
1098
1031
  this.config.appName
1099
1032
  );
1100
1033
  const fedAuthorsTimeout = new Promise((resolve) =>
1101
- setTimeout(() => resolve([]), 5000) // 5 second timeout, resolve with empty array
1034
+ setTimeout(() => resolve([]), 1000) // 1 second timeout, resolve with empty array
1102
1035
  );
1103
1036
  const federatedAuthors = await Promise.race([fedAuthorsPromise, fedAuthorsTimeout]);
1104
1037
  this._log('DEBUG', '✅ Got federated authors', { count: federatedAuthors?.length || 0 });
@@ -2311,17 +2244,18 @@ class HoloSphereBase extends HoloSphereCore {
2311
2244
  throw new TypeError('callback must be a function');
2312
2245
  }
2313
2246
  const path = storage.buildPath(this.config.appName, holonId, lensName);
2314
- const subscriptionOptions = { realtimeOnly: true, ...options };
2247
+ const subscriptionOptions = { realtimeOnly: true, appname: this.config.appName, ...options };
2315
2248
  const subscriptionId = `${holonId}-${lensName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
2316
2249
  let innerSubscription = null;
2317
2250
  let unsubscribeCalled = false;
2318
2251
 
2319
2252
  (async () => {
2320
2253
  try {
2321
- // Authorization check mirrors read() logic
2322
- const targetPubkey = await this._resolveHolonToPubkey(holonId);
2323
- const isOtherAuthor = targetPubkey && targetPubkey !== this.client.publicKey;
2254
+ // Quick sync check: 64-char hex pubkeys different from ours are clearly other-author
2255
+ const isPubkeyHolon = typeof holonId === 'string' && /^[0-9a-f]{64}$/i.test(holonId);
2256
+ const isOtherAuthor = isPubkeyHolon && holonId !== this.client.publicKey;
2324
2257
 
2258
+ // For other-author subscriptions, do authorization check before creating subscription
2325
2259
  if (isOtherAuthor) {
2326
2260
  const capToken = options.capabilityToken || options.capability;
2327
2261
  if (capToken) {
@@ -2331,29 +2265,52 @@ class HoloSphereBase extends HoloSphereCore {
2331
2265
  return;
2332
2266
  }
2333
2267
  } else {
2334
- const capability = await this._getCapabilityForAuthor(targetPubkey, { holonId, lensName });
2268
+ const capability = await this._getCapabilityForAuthor(holonId, { holonId, lensName });
2335
2269
  if (!capability) {
2336
2270
  this._log('WARN', '❌ Subscribe: no capability for federated author - skipping', {
2337
- holonId, lensName, targetPubkey: targetPubkey?.slice(0, 12) + '...'
2271
+ holonId, lensName, targetPubkey: holonId?.slice(0, 12) + '...'
2338
2272
  });
2339
2273
  return;
2340
2274
  }
2341
2275
  this._log('DEBUG', '✅ Subscribe: capability found for federated author', {
2342
- holonId, lensName, targetPubkey: targetPubkey?.slice(0, 12) + '...'
2276
+ holonId, lensName, targetPubkey: holonId?.slice(0, 12) + '...'
2343
2277
  });
2344
2278
  }
2345
2279
  }
2346
2280
 
2347
2281
  if (unsubscribeCalled) return;
2348
2282
 
2283
+ // Create subscription immediately with own public key as author filter
2284
+ subscriptionOptions.authors = [this.client.publicKey];
2285
+
2349
2286
  const subscription = await subscriptions.createSubscription(
2350
2287
  this.client, path, callback, subscriptionOptions
2351
2288
  );
2352
2289
  innerSubscription = subscription;
2353
2290
  if (unsubscribeCalled) {
2354
2291
  subscription.unsubscribe();
2355
- } else {
2356
- this.subscriptionRegistry.register(subscriptionId, subscription);
2292
+ return;
2293
+ }
2294
+ this.subscriptionRegistry.register(subscriptionId, subscription);
2295
+
2296
+ // Post-setup enhancement: include federated authors
2297
+ // Own-data subscription is already active; this adds partner visibility
2298
+ if (!isOtherAuthor) {
2299
+ try {
2300
+ const fedAuthorsPromise = registry.getFederatedAuthors(
2301
+ this.client,
2302
+ this.config.appName
2303
+ );
2304
+ const fedAuthorsTimeout = new Promise((resolve) =>
2305
+ setTimeout(() => resolve([]), 1000)
2306
+ );
2307
+ const federatedAuthors = await Promise.race([fedAuthorsPromise, fedAuthorsTimeout]);
2308
+ if (federatedAuthors.length > 0) {
2309
+ this._log('DEBUG', 'Subscribe: federated authors found post-setup', { count: federatedAuthors.length });
2310
+ }
2311
+ } catch (err) {
2312
+ this._log('WARN', 'Failed to get federated authors for subscription', { error: err.message });
2313
+ }
2357
2314
  }
2358
2315
  } catch (err) {
2359
2316
  this._log('ERROR', 'Subscription setup failed', { path, error: err.message });
@@ -878,8 +878,11 @@ export async function nostrSubscribeMany(client, pathPrefix, callback, options =
878
878
  const pathParts = pathPrefix.split('/');
879
879
  const targetAuthor = pathParts[1] || client.publicKey;
880
880
 
881
- // Create unique key for this subscription (use target author, not client pubkey)
882
- const subscriptionKey = `${targetAuthor}:${kind}:${pathPrefix}`;
881
+ // Use provided authors list (includes federated partners) or fall back to target author only
882
+ const allAuthors = options.authors || [targetAuthor];
883
+
884
+ // Create unique key for this subscription (include all authors to avoid conflicts)
885
+ const subscriptionKey = `${allAuthors.join(',')}:${kind}:${pathPrefix}`;
883
886
 
884
887
  // Check if we already have an active subscription for this path
885
888
  const existingSub = globalSubscriptions.get(subscriptionKey);
@@ -921,8 +924,8 @@ export async function nostrSubscribeMany(client, pathPrefix, callback, options =
921
924
  }
922
925
  seenEventIds.add(event.id);
923
926
 
924
- // Check if event is from a different author (relay not respecting filter)
925
- if (event.pubkey !== targetAuthor) {
927
+ // Check if event is from an allowed author (relay not respecting filter)
928
+ if (!allAuthors.includes(event.pubkey)) {
926
929
  rejectedCount++;
927
930
 
928
931
  // Only log periodically to avoid spam
@@ -931,7 +934,7 @@ export async function nostrSubscribeMany(client, pathPrefix, callback, options =
931
934
  console.warn('[nostrSubscribeMany] ⚠️ Relay not respecting authors filter!', {
932
935
  rejectedCount,
933
936
  acceptedCount,
934
- expected: targetAuthor,
937
+ expectedAuthors: allAuthors.length,
935
938
  message: 'Consider using a different relay or implementing private relay'
936
939
  });
937
940
  lastLogTime = now;
@@ -966,13 +969,14 @@ export async function nostrSubscribeMany(client, pathPrefix, callback, options =
966
969
 
967
970
  // Subscribe to data events (kind 30000) for initial load
968
971
  // Use 'since' to only get events from subscription time onward for real-time updates
969
- const subscriptionStartTime = Math.floor(Date.now() / 1000);
972
+ // Subtract 2 seconds to capture events written during async subscription setup
973
+ // (the subscribe() IIFE in index.js does auth/federation lookups before reaching here)
974
+ const subscriptionStartTime = Math.floor(Date.now() / 1000) - 2;
970
975
  const dataFilter = {
971
976
  kinds: [kind],
972
- authors: [targetAuthor], // Use target author (holon ID), not client pubkey
977
+ authors: allAuthors, // Include target author + federated partners
973
978
  since: subscriptionStartTime // Only get new events after subscription
974
979
  };
975
- console.log('[nostrSubscribeMany] Subscribing to:', { pathPrefix, targetAuthor: targetAuthor.slice(0, 12), clientPubKey: client.publicKey.slice(0, 12) });
976
980
  const dataSubscription = await client.subscribe(dataFilter, handleDataEvent);
977
981
 
978
982
  // Use Page Visibility API to refresh when tab becomes visible
@@ -523,8 +523,11 @@ export class NostrClient {
523
523
  dataDir: this.config.dataDir
524
524
  });
525
525
 
526
- // Load cached events from persistent storage
527
- await this._loadFromPersistentStorage();
526
+ // Load cached events from persistent storage in background
527
+ // (non-blocking to avoid stalling _initReady with large data sets)
528
+ this._loadFromPersistentStorage().catch(err => {
529
+ console.warn('Background persistent storage load failed:', err);
530
+ });
528
531
 
529
532
  // Initialize outbox queue for guaranteed delivery
530
533
  this.outboxQueue = new OutboxQueue(this.persistentStorage, {
@@ -23,7 +23,7 @@ import { resolveHologram } from '../federation/hologram.js';
23
23
  * @returns {Promise<Object>} Subscription object with path and unsubscribe method
24
24
  */
25
25
  export async function createSubscription(client, path, callback, options = {}) {
26
- const { throttle = 0, filter = null, includeFederated = false, triggerInitial = false, realtimeOnly = true, resolveHolograms = true } = options;
26
+ const { throttle = 0, filter = null, includeFederated = false, triggerInitial = false, realtimeOnly = true, resolveHolograms = true, appname = null } = options;
27
27
 
28
28
  let lastInvoke = 0;
29
29
  let timeoutId = null;
@@ -48,13 +48,22 @@ export async function createSubscription(client, path, callback, options = {}) {
48
48
  let resolvedData = data;
49
49
  if (resolveHolograms && data && data.hologram === true) {
50
50
  try {
51
- resolvedData = await resolveHologram(client, data);
51
+ resolvedData = await resolveHologram(client, data, undefined, undefined, { appname });
52
52
  // If resolution failed, skip this item (source data may not exist yet)
53
53
  if (!resolvedData) {
54
+ console.debug('[subscription] Hologram resolution returned null, skipping:', {
55
+ id: data.id,
56
+ soul: data.soul,
57
+ target: data.target?.holonId?.slice(0, 12)
58
+ });
54
59
  return;
55
60
  }
56
61
  } catch (err) {
57
- console.warn('Failed to resolve hologram in subscription:', err);
62
+ console.warn('Failed to resolve hologram in subscription:', err, {
63
+ id: data?.id,
64
+ soul: data?.soul,
65
+ target: data?.target?.holonId?.slice(0, 12)
66
+ });
58
67
  return;
59
68
  }
60
69
  }
@@ -85,7 +94,7 @@ export async function createSubscription(client, path, callback, options = {}) {
85
94
  };
86
95
 
87
96
  // Subscribe using Nostr wrapper
88
- const subscription = await subscribe(client, path, wrappedCallback, { realtimeOnly });
97
+ const subscription = await subscribe(client, path, wrappedCallback, { realtimeOnly, ...(options.authors && { authors: options.authors }) });
89
98
 
90
99
  // Return subscription object
91
100
  return {