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 +1 -1
- package/src/index.js +46 -89
- package/src/storage/nostr-async.js +12 -8
- package/src/storage/nostr-client.js +5 -2
- package/src/subscriptions/manager.js +13 -4
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -875,21 +875,22 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
875
875
|
}
|
|
876
876
|
|
|
877
877
|
/**
|
|
878
|
-
*
|
|
879
|
-
*
|
|
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
|
|
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
|
|
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 &&
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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([]),
|
|
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
|
-
//
|
|
2322
|
-
const
|
|
2323
|
-
const isOtherAuthor =
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
2356
|
-
|
|
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
|
-
//
|
|
882
|
-
const
|
|
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
|
|
925
|
-
if (event.pubkey
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 {
|