@unicitylabs/sphere-sdk 0.7.1-dev.1 → 0.7.1-dev.3
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/dist/core/index.cjs +230 -81
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +70 -5
- package/dist/core/index.d.ts +70 -5
- package/dist/core/index.js +230 -81
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +11 -0
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +11 -0
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +11 -0
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +10 -1
- package/dist/impl/nodejs/index.d.ts +10 -1
- package/dist/impl/nodejs/index.js +11 -0
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +230 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +230 -81
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/core/index.d.cts
CHANGED
|
@@ -768,8 +768,22 @@ interface MultiAddressTransportMuxConfig {
|
|
|
768
768
|
storage?: TransportStorageAdapter;
|
|
769
769
|
/** Private key for the Mux's NostrClient identity. If provided, the Mux
|
|
770
770
|
* authenticates as this key — required for relays that filter gift-wrap
|
|
771
|
-
* event delivery to the recipient's subscription.
|
|
771
|
+
* event delivery to the recipient's subscription.
|
|
772
|
+
* Ignored when {@link sharedNostrClient} is set. */
|
|
772
773
|
identityPrivateKey?: Uint8Array;
|
|
774
|
+
/**
|
|
775
|
+
* Optional pre-existing {@link NostrClient} to reuse instead of opening a
|
|
776
|
+
* fresh WebSocket per relay (#123). When set, the Mux skips both the
|
|
777
|
+
* {@code new NostrClient(...)} construction and {@code connect()} — it
|
|
778
|
+
* only registers subscription/connection listeners on the shared
|
|
779
|
+
* client. The Mux does NOT take ownership: its {@code disconnect()}
|
|
780
|
+
* leaves the client connected, since the caller (e.g. the original
|
|
781
|
+
* {@link NostrTransportProvider}) still uses it for resolve calls.
|
|
782
|
+
*
|
|
783
|
+
* Use a getter when the client may be created lazily (e.g. before the
|
|
784
|
+
* provider has connected).
|
|
785
|
+
*/
|
|
786
|
+
sharedNostrClient?: NostrClient | null | (() => NostrClient | null);
|
|
773
787
|
}
|
|
774
788
|
declare class MultiAddressTransportMux {
|
|
775
789
|
private config;
|
|
@@ -783,14 +797,14 @@ declare class MultiAddressTransportMux {
|
|
|
783
797
|
private chatSubscriptionId;
|
|
784
798
|
private chatEoseFired;
|
|
785
799
|
private resubscribeTimer;
|
|
786
|
-
private lastWalletEventAt;
|
|
787
|
-
private lastChatEventAt;
|
|
788
|
-
private healthCheckTimer;
|
|
789
800
|
private chatEoseHandlers;
|
|
790
801
|
private processedEventIds;
|
|
791
802
|
private static readonly MAX_PROCESSED_IDS;
|
|
792
803
|
private eventCallbacks;
|
|
793
804
|
private readonly identityPrivateKey;
|
|
805
|
+
private readonly sharedNostrClientGetter;
|
|
806
|
+
private usingSharedClient;
|
|
807
|
+
private connectionListener;
|
|
794
808
|
constructor(config: MultiAddressTransportMuxConfig);
|
|
795
809
|
/**
|
|
796
810
|
* Add an address to the multiplexer.
|
|
@@ -815,6 +829,58 @@ declare class MultiAddressTransportMux {
|
|
|
815
829
|
connect(): Promise<void>;
|
|
816
830
|
disconnect(): Promise<void>;
|
|
817
831
|
isConnected(): boolean;
|
|
832
|
+
/**
|
|
833
|
+
* Build the connection listener used by both {@link connect} and
|
|
834
|
+
* {@link rebindToSharedClient}.
|
|
835
|
+
*
|
|
836
|
+
* Behavioral notes:
|
|
837
|
+
* - When the Mux is sharing a {@link NostrClient} with the host
|
|
838
|
+
* transport (#123), we deliberately do NOT emit
|
|
839
|
+
* {@code transport:connected} / {@code transport:reconnecting} here
|
|
840
|
+
* — the host transport's own listener already emits those for the
|
|
841
|
+
* same socket event. Re-subscribing after a reconnect IS still our
|
|
842
|
+
* responsibility, since the host has
|
|
843
|
+
* {@code suppressSubscriptions()}'d its own filters.
|
|
844
|
+
* - {@code onConnect} does not emit {@code transport:connected}.
|
|
845
|
+
* The SDK only fires {@code onConnect} on the initial socket
|
|
846
|
+
* connection (subsequent reconnects use {@code onReconnected}),
|
|
847
|
+
* and {@link connect()}'s bottom already emits
|
|
848
|
+
* {@code transport:connected} once that returns. Emitting here too
|
|
849
|
+
* would double-fire on every initial connect.
|
|
850
|
+
* - Each callback bails out early when the Mux is not in an active
|
|
851
|
+
* state ({@code disconnected} / {@code error}). Listeners are
|
|
852
|
+
* removed on {@code disconnect()} before the callback can fire,
|
|
853
|
+
* so this guard is mainly defense-in-depth against any in-flight
|
|
854
|
+
* callback that lands during teardown — but having it at the top
|
|
855
|
+
* means we never emit a misleading {@code transport:connected}
|
|
856
|
+
* from a Mux that has already torn down.
|
|
857
|
+
*/
|
|
858
|
+
private buildConnectionListener;
|
|
859
|
+
/**
|
|
860
|
+
* Re-attach to a freshly-created shared NostrClient.
|
|
861
|
+
*
|
|
862
|
+
* Call this after the host (e.g. {@link NostrTransportProvider}) has
|
|
863
|
+
* recreated its NostrClient — typically because the wallet's active
|
|
864
|
+
* identity changed and the SDK's NostrClient does not support
|
|
865
|
+
* changing identity at runtime. The previous client has already
|
|
866
|
+
* been disconnected by the host, so its server-side subscriptions
|
|
867
|
+
* are gone — we just adopt the new client and re-issue our own.
|
|
868
|
+
*
|
|
869
|
+
* The caller is responsible for ordering: by the time rebind runs,
|
|
870
|
+
* the host transport's new NostrClient must already be created and
|
|
871
|
+
* connected. In Sphere this is guaranteed because we await
|
|
872
|
+
* {@code transport.setIdentity()} before calling rebind.
|
|
873
|
+
*
|
|
874
|
+
* Returns silently in two cases that are not caller errors:
|
|
875
|
+
* - the Mux owns its own client (not sharing) — nothing to rebind
|
|
876
|
+
* - the shared client reference hasn't changed (rebind is a no-op)
|
|
877
|
+
*
|
|
878
|
+
* Throws otherwise (rather than silently no-op'ing) so a wiring
|
|
879
|
+
* mistake — for instance, calling rebind before the host's new
|
|
880
|
+
* client is ready — surfaces immediately instead of leaving the
|
|
881
|
+
* Mux pinned to a stale client.
|
|
882
|
+
*/
|
|
883
|
+
rebindToSharedClient(): Promise<void>;
|
|
818
884
|
/**
|
|
819
885
|
* One-shot fetch of pending events from the relay.
|
|
820
886
|
* Creates a temporary subscription, waits for EOSE (or timeout),
|
|
@@ -835,7 +901,6 @@ declare class MultiAddressTransportMux {
|
|
|
835
901
|
* Called whenever addresses are added/removed.
|
|
836
902
|
*/
|
|
837
903
|
private updateSubscriptions;
|
|
838
|
-
private startHealthCheck;
|
|
839
904
|
/**
|
|
840
905
|
* Schedule a re-subscription after a relay-initiated subscription closure.
|
|
841
906
|
* Debounced: if both wallet and chat subscriptions fire onError in quick
|
package/dist/core/index.d.ts
CHANGED
|
@@ -768,8 +768,22 @@ interface MultiAddressTransportMuxConfig {
|
|
|
768
768
|
storage?: TransportStorageAdapter;
|
|
769
769
|
/** Private key for the Mux's NostrClient identity. If provided, the Mux
|
|
770
770
|
* authenticates as this key — required for relays that filter gift-wrap
|
|
771
|
-
* event delivery to the recipient's subscription.
|
|
771
|
+
* event delivery to the recipient's subscription.
|
|
772
|
+
* Ignored when {@link sharedNostrClient} is set. */
|
|
772
773
|
identityPrivateKey?: Uint8Array;
|
|
774
|
+
/**
|
|
775
|
+
* Optional pre-existing {@link NostrClient} to reuse instead of opening a
|
|
776
|
+
* fresh WebSocket per relay (#123). When set, the Mux skips both the
|
|
777
|
+
* {@code new NostrClient(...)} construction and {@code connect()} — it
|
|
778
|
+
* only registers subscription/connection listeners on the shared
|
|
779
|
+
* client. The Mux does NOT take ownership: its {@code disconnect()}
|
|
780
|
+
* leaves the client connected, since the caller (e.g. the original
|
|
781
|
+
* {@link NostrTransportProvider}) still uses it for resolve calls.
|
|
782
|
+
*
|
|
783
|
+
* Use a getter when the client may be created lazily (e.g. before the
|
|
784
|
+
* provider has connected).
|
|
785
|
+
*/
|
|
786
|
+
sharedNostrClient?: NostrClient | null | (() => NostrClient | null);
|
|
773
787
|
}
|
|
774
788
|
declare class MultiAddressTransportMux {
|
|
775
789
|
private config;
|
|
@@ -783,14 +797,14 @@ declare class MultiAddressTransportMux {
|
|
|
783
797
|
private chatSubscriptionId;
|
|
784
798
|
private chatEoseFired;
|
|
785
799
|
private resubscribeTimer;
|
|
786
|
-
private lastWalletEventAt;
|
|
787
|
-
private lastChatEventAt;
|
|
788
|
-
private healthCheckTimer;
|
|
789
800
|
private chatEoseHandlers;
|
|
790
801
|
private processedEventIds;
|
|
791
802
|
private static readonly MAX_PROCESSED_IDS;
|
|
792
803
|
private eventCallbacks;
|
|
793
804
|
private readonly identityPrivateKey;
|
|
805
|
+
private readonly sharedNostrClientGetter;
|
|
806
|
+
private usingSharedClient;
|
|
807
|
+
private connectionListener;
|
|
794
808
|
constructor(config: MultiAddressTransportMuxConfig);
|
|
795
809
|
/**
|
|
796
810
|
* Add an address to the multiplexer.
|
|
@@ -815,6 +829,58 @@ declare class MultiAddressTransportMux {
|
|
|
815
829
|
connect(): Promise<void>;
|
|
816
830
|
disconnect(): Promise<void>;
|
|
817
831
|
isConnected(): boolean;
|
|
832
|
+
/**
|
|
833
|
+
* Build the connection listener used by both {@link connect} and
|
|
834
|
+
* {@link rebindToSharedClient}.
|
|
835
|
+
*
|
|
836
|
+
* Behavioral notes:
|
|
837
|
+
* - When the Mux is sharing a {@link NostrClient} with the host
|
|
838
|
+
* transport (#123), we deliberately do NOT emit
|
|
839
|
+
* {@code transport:connected} / {@code transport:reconnecting} here
|
|
840
|
+
* — the host transport's own listener already emits those for the
|
|
841
|
+
* same socket event. Re-subscribing after a reconnect IS still our
|
|
842
|
+
* responsibility, since the host has
|
|
843
|
+
* {@code suppressSubscriptions()}'d its own filters.
|
|
844
|
+
* - {@code onConnect} does not emit {@code transport:connected}.
|
|
845
|
+
* The SDK only fires {@code onConnect} on the initial socket
|
|
846
|
+
* connection (subsequent reconnects use {@code onReconnected}),
|
|
847
|
+
* and {@link connect()}'s bottom already emits
|
|
848
|
+
* {@code transport:connected} once that returns. Emitting here too
|
|
849
|
+
* would double-fire on every initial connect.
|
|
850
|
+
* - Each callback bails out early when the Mux is not in an active
|
|
851
|
+
* state ({@code disconnected} / {@code error}). Listeners are
|
|
852
|
+
* removed on {@code disconnect()} before the callback can fire,
|
|
853
|
+
* so this guard is mainly defense-in-depth against any in-flight
|
|
854
|
+
* callback that lands during teardown — but having it at the top
|
|
855
|
+
* means we never emit a misleading {@code transport:connected}
|
|
856
|
+
* from a Mux that has already torn down.
|
|
857
|
+
*/
|
|
858
|
+
private buildConnectionListener;
|
|
859
|
+
/**
|
|
860
|
+
* Re-attach to a freshly-created shared NostrClient.
|
|
861
|
+
*
|
|
862
|
+
* Call this after the host (e.g. {@link NostrTransportProvider}) has
|
|
863
|
+
* recreated its NostrClient — typically because the wallet's active
|
|
864
|
+
* identity changed and the SDK's NostrClient does not support
|
|
865
|
+
* changing identity at runtime. The previous client has already
|
|
866
|
+
* been disconnected by the host, so its server-side subscriptions
|
|
867
|
+
* are gone — we just adopt the new client and re-issue our own.
|
|
868
|
+
*
|
|
869
|
+
* The caller is responsible for ordering: by the time rebind runs,
|
|
870
|
+
* the host transport's new NostrClient must already be created and
|
|
871
|
+
* connected. In Sphere this is guaranteed because we await
|
|
872
|
+
* {@code transport.setIdentity()} before calling rebind.
|
|
873
|
+
*
|
|
874
|
+
* Returns silently in two cases that are not caller errors:
|
|
875
|
+
* - the Mux owns its own client (not sharing) — nothing to rebind
|
|
876
|
+
* - the shared client reference hasn't changed (rebind is a no-op)
|
|
877
|
+
*
|
|
878
|
+
* Throws otherwise (rather than silently no-op'ing) so a wiring
|
|
879
|
+
* mistake — for instance, calling rebind before the host's new
|
|
880
|
+
* client is ready — surfaces immediately instead of leaving the
|
|
881
|
+
* Mux pinned to a stale client.
|
|
882
|
+
*/
|
|
883
|
+
rebindToSharedClient(): Promise<void>;
|
|
818
884
|
/**
|
|
819
885
|
* One-shot fetch of pending events from the relay.
|
|
820
886
|
* Creates a temporary subscription, waits for EOSE (or timeout),
|
|
@@ -835,7 +901,6 @@ declare class MultiAddressTransportMux {
|
|
|
835
901
|
* Called whenever addresses are added/removed.
|
|
836
902
|
*/
|
|
837
903
|
private updateSubscriptions;
|
|
838
|
-
private startHealthCheck;
|
|
839
904
|
/**
|
|
840
905
|
* Schedule a re-subscription after a relay-initiated subscription closure.
|
|
841
906
|
* Debounced: if both wallet and chat subscriptions fire onError in quick
|
package/dist/core/index.js
CHANGED
|
@@ -1365,6 +1365,17 @@ var NostrTransportProvider = class _NostrTransportProvider {
|
|
|
1365
1365
|
getStorageAdapter() {
|
|
1366
1366
|
return this.storage;
|
|
1367
1367
|
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Get the underlying NostrClient (or null if not yet connected).
|
|
1370
|
+
*
|
|
1371
|
+
* Exposed so {@link MultiAddressTransportMux} can share the same
|
|
1372
|
+
* client/socket pair instead of opening a duplicate WebSocket per
|
|
1373
|
+
* relay (#123). The transport owns the client's lifecycle — callers
|
|
1374
|
+
* MUST NOT call {@code disconnect()} on the returned instance.
|
|
1375
|
+
*/
|
|
1376
|
+
getNostrClient() {
|
|
1377
|
+
return this.nostrClient;
|
|
1378
|
+
}
|
|
1368
1379
|
/**
|
|
1369
1380
|
* Suppress event subscriptions — unsubscribe wallet/chat filters
|
|
1370
1381
|
* but keep the connection alive for resolve/identity-binding operations.
|
|
@@ -3003,9 +3014,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3003
3014
|
chatSubscriptionId = null;
|
|
3004
3015
|
chatEoseFired = false;
|
|
3005
3016
|
resubscribeTimer = null;
|
|
3006
|
-
lastWalletEventAt = Date.now();
|
|
3007
|
-
lastChatEventAt = Date.now();
|
|
3008
|
-
healthCheckTimer = null;
|
|
3009
3017
|
chatEoseHandlers = [];
|
|
3010
3018
|
// Dedup — bounded to prevent memory leak in long-running sessions.
|
|
3011
3019
|
// Set preserves insertion order; evict oldest entries when cap is reached.
|
|
@@ -3016,6 +3024,19 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3016
3024
|
// Identity key for the Mux's NostrClient — relays may filter gift-wrap
|
|
3017
3025
|
// delivery to the recipient's subscription key.
|
|
3018
3026
|
identityPrivateKey;
|
|
3027
|
+
// Resolves the shared NostrClient at use-time (the source provider may
|
|
3028
|
+
// create its client lazily, after the Mux is constructed). null means
|
|
3029
|
+
// "no shared client; create our own."
|
|
3030
|
+
sharedNostrClientGetter;
|
|
3031
|
+
// True when this Mux is using a shared NostrClient and therefore must
|
|
3032
|
+
// not call connect()/disconnect() on it.
|
|
3033
|
+
usingSharedClient = false;
|
|
3034
|
+
// Listener registered on the underlying NostrClient. Tracked so we can
|
|
3035
|
+
// remove it on disconnect / rebind — otherwise a long-lived shared
|
|
3036
|
+
// client accumulates listeners across address switches and (worse)
|
|
3037
|
+
// a "disconnected" Mux still sees onReconnected callbacks fire and
|
|
3038
|
+
// re-establish subscriptions it shouldn't have.
|
|
3039
|
+
connectionListener = null;
|
|
3019
3040
|
constructor(config) {
|
|
3020
3041
|
this.identityPrivateKey = config.identityPrivateKey;
|
|
3021
3042
|
this.config = {
|
|
@@ -3028,6 +3049,14 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3028
3049
|
generateUUID: config.generateUUID ?? defaultUUIDGenerator
|
|
3029
3050
|
};
|
|
3030
3051
|
this.storage = config.storage ?? null;
|
|
3052
|
+
if (typeof config.sharedNostrClient === "function") {
|
|
3053
|
+
this.sharedNostrClientGetter = config.sharedNostrClient;
|
|
3054
|
+
} else if (config.sharedNostrClient) {
|
|
3055
|
+
const c = config.sharedNostrClient;
|
|
3056
|
+
this.sharedNostrClientGetter = () => c;
|
|
3057
|
+
} else {
|
|
3058
|
+
this.sharedNostrClientGetter = null;
|
|
3059
|
+
}
|
|
3031
3060
|
}
|
|
3032
3061
|
// ===========================================================================
|
|
3033
3062
|
// Address Management
|
|
@@ -3118,53 +3147,49 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3118
3147
|
if (this.status === "connected") return;
|
|
3119
3148
|
this.status = "connecting";
|
|
3120
3149
|
try {
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3150
|
+
const shared = this.sharedNostrClientGetter ? this.sharedNostrClientGetter() : null;
|
|
3151
|
+
if (shared) {
|
|
3152
|
+
if (!shared.isConnected()) {
|
|
3153
|
+
throw new SphereError(
|
|
3154
|
+
"sharedNostrClient is not connected; the Mux cannot share a closed socket",
|
|
3155
|
+
"TRANSPORT_ERROR"
|
|
3125
3156
|
);
|
|
3126
|
-
} else {
|
|
3127
|
-
const tempKey = Buffer3.alloc(32);
|
|
3128
|
-
crypto.getRandomValues(tempKey);
|
|
3129
|
-
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(tempKey);
|
|
3130
3157
|
}
|
|
3158
|
+
this.nostrClient = shared;
|
|
3159
|
+
this.usingSharedClient = true;
|
|
3160
|
+
} else {
|
|
3161
|
+
if (!this.primaryKeyManager) {
|
|
3162
|
+
if (this.identityPrivateKey) {
|
|
3163
|
+
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(
|
|
3164
|
+
Buffer3.from(this.identityPrivateKey)
|
|
3165
|
+
);
|
|
3166
|
+
} else {
|
|
3167
|
+
const tempKey = Buffer3.alloc(32);
|
|
3168
|
+
crypto.getRandomValues(tempKey);
|
|
3169
|
+
this.primaryKeyManager = NostrKeyManager2.fromPrivateKey(tempKey);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
this.nostrClient = new NostrClient2(this.primaryKeyManager, {
|
|
3173
|
+
autoReconnect: this.config.autoReconnect,
|
|
3174
|
+
reconnectIntervalMs: this.config.reconnectDelay,
|
|
3175
|
+
maxReconnectIntervalMs: this.config.reconnectDelay * 16,
|
|
3176
|
+
pingIntervalMs: 15e3
|
|
3177
|
+
});
|
|
3131
3178
|
}
|
|
3132
|
-
this.
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
},
|
|
3146
|
-
onReconnecting: (url, attempt) => {
|
|
3147
|
-
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3148
|
-
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3149
|
-
},
|
|
3150
|
-
onReconnected: (url) => {
|
|
3151
|
-
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3152
|
-
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3153
|
-
this.updateSubscriptions().catch((err) => {
|
|
3154
|
-
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3155
|
-
});
|
|
3179
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3180
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3181
|
+
if (!this.usingSharedClient) {
|
|
3182
|
+
await Promise.race([
|
|
3183
|
+
this.nostrClient.connect(...this.config.relays),
|
|
3184
|
+
new Promise(
|
|
3185
|
+
(_, reject) => setTimeout(() => reject(new Error(
|
|
3186
|
+
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3187
|
+
)), this.config.timeout)
|
|
3188
|
+
)
|
|
3189
|
+
]);
|
|
3190
|
+
if (!this.nostrClient.isConnected()) {
|
|
3191
|
+
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3156
3192
|
}
|
|
3157
|
-
});
|
|
3158
|
-
await Promise.race([
|
|
3159
|
-
this.nostrClient.connect(...this.config.relays),
|
|
3160
|
-
new Promise(
|
|
3161
|
-
(_, reject) => setTimeout(() => reject(new Error(
|
|
3162
|
-
`Transport connection timed out after ${this.config.timeout}ms`
|
|
3163
|
-
)), this.config.timeout)
|
|
3164
|
-
)
|
|
3165
|
-
]);
|
|
3166
|
-
if (!this.nostrClient.isConnected()) {
|
|
3167
|
-
throw new SphereError("Failed to connect to any relay", "TRANSPORT_ERROR");
|
|
3168
3193
|
}
|
|
3169
3194
|
this.status = "connected";
|
|
3170
3195
|
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
@@ -3173,6 +3198,21 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3173
3198
|
}
|
|
3174
3199
|
} catch (error) {
|
|
3175
3200
|
this.status = "error";
|
|
3201
|
+
if (this.connectionListener && this.nostrClient) {
|
|
3202
|
+
try {
|
|
3203
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3204
|
+
} catch {
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
this.connectionListener = null;
|
|
3208
|
+
if (this.nostrClient && !this.usingSharedClient) {
|
|
3209
|
+
try {
|
|
3210
|
+
this.nostrClient.disconnect();
|
|
3211
|
+
} catch {
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
this.nostrClient = null;
|
|
3215
|
+
this.usingSharedClient = false;
|
|
3176
3216
|
throw error;
|
|
3177
3217
|
}
|
|
3178
3218
|
}
|
|
@@ -3181,25 +3221,153 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3181
3221
|
clearTimeout(this.resubscribeTimer);
|
|
3182
3222
|
this.resubscribeTimer = null;
|
|
3183
3223
|
}
|
|
3184
|
-
if (this.healthCheckTimer) {
|
|
3185
|
-
clearInterval(this.healthCheckTimer);
|
|
3186
|
-
this.healthCheckTimer = null;
|
|
3187
|
-
}
|
|
3188
3224
|
if (this.nostrClient) {
|
|
3189
|
-
this.
|
|
3225
|
+
if (this.walletSubscriptionId) {
|
|
3226
|
+
try {
|
|
3227
|
+
this.nostrClient.unsubscribe(this.walletSubscriptionId);
|
|
3228
|
+
} catch {
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (this.chatSubscriptionId) {
|
|
3232
|
+
try {
|
|
3233
|
+
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3234
|
+
} catch {
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
if (this.connectionListener) {
|
|
3238
|
+
try {
|
|
3239
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3240
|
+
} catch {
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (!this.usingSharedClient) {
|
|
3244
|
+
this.nostrClient.disconnect();
|
|
3245
|
+
}
|
|
3190
3246
|
this.nostrClient = null;
|
|
3191
3247
|
}
|
|
3248
|
+
this.connectionListener = null;
|
|
3249
|
+
this.usingSharedClient = false;
|
|
3192
3250
|
this.walletSubscriptionId = null;
|
|
3193
3251
|
this.chatSubscriptionId = null;
|
|
3194
3252
|
this.chatEoseFired = false;
|
|
3195
|
-
this.lastWalletEventAt = Date.now();
|
|
3196
|
-
this.lastChatEventAt = Date.now();
|
|
3197
3253
|
this.status = "disconnected";
|
|
3198
3254
|
this.emitEvent({ type: "transport:disconnected", timestamp: Date.now() });
|
|
3199
3255
|
}
|
|
3200
3256
|
isConnected() {
|
|
3201
3257
|
return this.status === "connected" && this.nostrClient?.isConnected() === true;
|
|
3202
3258
|
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Build the connection listener used by both {@link connect} and
|
|
3261
|
+
* {@link rebindToSharedClient}.
|
|
3262
|
+
*
|
|
3263
|
+
* Behavioral notes:
|
|
3264
|
+
* - When the Mux is sharing a {@link NostrClient} with the host
|
|
3265
|
+
* transport (#123), we deliberately do NOT emit
|
|
3266
|
+
* {@code transport:connected} / {@code transport:reconnecting} here
|
|
3267
|
+
* — the host transport's own listener already emits those for the
|
|
3268
|
+
* same socket event. Re-subscribing after a reconnect IS still our
|
|
3269
|
+
* responsibility, since the host has
|
|
3270
|
+
* {@code suppressSubscriptions()}'d its own filters.
|
|
3271
|
+
* - {@code onConnect} does not emit {@code transport:connected}.
|
|
3272
|
+
* The SDK only fires {@code onConnect} on the initial socket
|
|
3273
|
+
* connection (subsequent reconnects use {@code onReconnected}),
|
|
3274
|
+
* and {@link connect()}'s bottom already emits
|
|
3275
|
+
* {@code transport:connected} once that returns. Emitting here too
|
|
3276
|
+
* would double-fire on every initial connect.
|
|
3277
|
+
* - Each callback bails out early when the Mux is not in an active
|
|
3278
|
+
* state ({@code disconnected} / {@code error}). Listeners are
|
|
3279
|
+
* removed on {@code disconnect()} before the callback can fire,
|
|
3280
|
+
* so this guard is mainly defense-in-depth against any in-flight
|
|
3281
|
+
* callback that lands during teardown — but having it at the top
|
|
3282
|
+
* means we never emit a misleading {@code transport:connected}
|
|
3283
|
+
* from a Mux that has already torn down.
|
|
3284
|
+
*/
|
|
3285
|
+
buildConnectionListener() {
|
|
3286
|
+
const isInactive = () => this.status === "disconnected" || this.status === "error";
|
|
3287
|
+
return {
|
|
3288
|
+
onConnect: (url) => {
|
|
3289
|
+
if (isInactive()) return;
|
|
3290
|
+
logger.debug("Mux", "Connected to relay:", url);
|
|
3291
|
+
},
|
|
3292
|
+
onDisconnect: (url, reason) => {
|
|
3293
|
+
logger.debug("Mux", "Disconnected from relay:", url, "reason:", reason);
|
|
3294
|
+
},
|
|
3295
|
+
onReconnecting: (url, attempt) => {
|
|
3296
|
+
if (isInactive()) return;
|
|
3297
|
+
logger.debug("Mux", "Reconnecting to relay:", url, "attempt:", attempt);
|
|
3298
|
+
if (!this.usingSharedClient) {
|
|
3299
|
+
this.emitEvent({ type: "transport:reconnecting", timestamp: Date.now() });
|
|
3300
|
+
}
|
|
3301
|
+
},
|
|
3302
|
+
onReconnected: (url) => {
|
|
3303
|
+
if (isInactive()) return;
|
|
3304
|
+
logger.debug("Mux", "Reconnected to relay:", url);
|
|
3305
|
+
if (!this.usingSharedClient) {
|
|
3306
|
+
this.emitEvent({ type: "transport:connected", timestamp: Date.now() });
|
|
3307
|
+
}
|
|
3308
|
+
this.updateSubscriptions().catch((err) => {
|
|
3309
|
+
logger.error("Mux", "Failed to re-subscribe after reconnect:", err);
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Re-attach to a freshly-created shared NostrClient.
|
|
3316
|
+
*
|
|
3317
|
+
* Call this after the host (e.g. {@link NostrTransportProvider}) has
|
|
3318
|
+
* recreated its NostrClient — typically because the wallet's active
|
|
3319
|
+
* identity changed and the SDK's NostrClient does not support
|
|
3320
|
+
* changing identity at runtime. The previous client has already
|
|
3321
|
+
* been disconnected by the host, so its server-side subscriptions
|
|
3322
|
+
* are gone — we just adopt the new client and re-issue our own.
|
|
3323
|
+
*
|
|
3324
|
+
* The caller is responsible for ordering: by the time rebind runs,
|
|
3325
|
+
* the host transport's new NostrClient must already be created and
|
|
3326
|
+
* connected. In Sphere this is guaranteed because we await
|
|
3327
|
+
* {@code transport.setIdentity()} before calling rebind.
|
|
3328
|
+
*
|
|
3329
|
+
* Returns silently in two cases that are not caller errors:
|
|
3330
|
+
* - the Mux owns its own client (not sharing) — nothing to rebind
|
|
3331
|
+
* - the shared client reference hasn't changed (rebind is a no-op)
|
|
3332
|
+
*
|
|
3333
|
+
* Throws otherwise (rather than silently no-op'ing) so a wiring
|
|
3334
|
+
* mistake — for instance, calling rebind before the host's new
|
|
3335
|
+
* client is ready — surfaces immediately instead of leaving the
|
|
3336
|
+
* Mux pinned to a stale client.
|
|
3337
|
+
*/
|
|
3338
|
+
async rebindToSharedClient() {
|
|
3339
|
+
if (!this.usingSharedClient) return;
|
|
3340
|
+
if (!this.sharedNostrClientGetter) return;
|
|
3341
|
+
const newClient = this.sharedNostrClientGetter();
|
|
3342
|
+
if (!newClient) {
|
|
3343
|
+
throw new SphereError(
|
|
3344
|
+
"rebindToSharedClient: shared client getter returned null. The host transport must finish (re)creating its NostrClient before rebind is called.",
|
|
3345
|
+
"TRANSPORT_ERROR"
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
if (this.nostrClient === newClient) return;
|
|
3349
|
+
if (!newClient.isConnected()) {
|
|
3350
|
+
throw new SphereError(
|
|
3351
|
+
"rebindToSharedClient: new shared client is not connected. Await transport.setIdentity() / transport.connect() before rebinding.",
|
|
3352
|
+
"TRANSPORT_ERROR"
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
if (this.nostrClient && this.connectionListener && this.nostrClient !== newClient) {
|
|
3356
|
+
try {
|
|
3357
|
+
this.nostrClient.removeConnectionListener(this.connectionListener);
|
|
3358
|
+
} catch {
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
this.nostrClient = newClient;
|
|
3362
|
+
this.walletSubscriptionId = null;
|
|
3363
|
+
this.chatSubscriptionId = null;
|
|
3364
|
+
this.chatEoseFired = false;
|
|
3365
|
+
this.connectionListener = this.buildConnectionListener();
|
|
3366
|
+
this.nostrClient.addConnectionListener(this.connectionListener);
|
|
3367
|
+
if (this.addresses.size > 0) {
|
|
3368
|
+
await this.updateSubscriptions();
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3203
3371
|
/**
|
|
3204
3372
|
* One-shot fetch of pending events from the relay.
|
|
3205
3373
|
* Creates a temporary subscription, waits for EOSE (or timeout),
|
|
@@ -3328,8 +3496,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3328
3496
|
this.nostrClient.unsubscribe(this.chatSubscriptionId);
|
|
3329
3497
|
this.chatSubscriptionId = null;
|
|
3330
3498
|
}
|
|
3331
|
-
this.lastWalletEventAt = Date.now();
|
|
3332
|
-
this.lastChatEventAt = Date.now();
|
|
3333
3499
|
if (this.addresses.size === 0) return;
|
|
3334
3500
|
const allPubkeys = [];
|
|
3335
3501
|
for (const entry of this.addresses.values()) {
|
|
@@ -3416,25 +3582,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3416
3582
|
}
|
|
3417
3583
|
});
|
|
3418
3584
|
logger.debug("Mux", `updateSubscriptions: walletSub=${this.walletSubscriptionId} chatSub=${this.chatSubscriptionId}`);
|
|
3419
|
-
this.startHealthCheck();
|
|
3420
|
-
}
|
|
3421
|
-
startHealthCheck() {
|
|
3422
|
-
if (this.healthCheckTimer) return;
|
|
3423
|
-
this.healthCheckTimer = setInterval(() => {
|
|
3424
|
-
if (!this.isConnected()) return;
|
|
3425
|
-
const chatElapsed = Date.now() - this.lastChatEventAt;
|
|
3426
|
-
const walletElapsed = Date.now() - this.lastWalletEventAt;
|
|
3427
|
-
const needResubscribe = chatElapsed > 6e4 || walletElapsed > 3e5;
|
|
3428
|
-
if (needResubscribe) {
|
|
3429
|
-
const reason = chatElapsed > 6e4 ? `No chat events for ${Math.round(chatElapsed / 1e3)}s` : `No wallet events for ${Math.round(walletElapsed / 1e3)}s`;
|
|
3430
|
-
logger.warn("Mux", `${reason} \u2014 re-subscribing`);
|
|
3431
|
-
this.lastChatEventAt = Date.now();
|
|
3432
|
-
this.lastWalletEventAt = Date.now();
|
|
3433
|
-
this.updateSubscriptions().catch((err) => {
|
|
3434
|
-
logger.warn("Mux", "Health check re-subscription failed:", err);
|
|
3435
|
-
});
|
|
3436
|
-
}
|
|
3437
|
-
}, 3e4);
|
|
3438
3585
|
}
|
|
3439
3586
|
/**
|
|
3440
3587
|
* Schedule a re-subscription after a relay-initiated subscription closure.
|
|
@@ -3495,12 +3642,6 @@ var MultiAddressTransportMux = class _MultiAddressTransportMux {
|
|
|
3495
3642
|
}
|
|
3496
3643
|
}
|
|
3497
3644
|
}
|
|
3498
|
-
if (event.kind !== EventKinds2.GIFT_WRAP) {
|
|
3499
|
-
this.lastWalletEventAt = Date.now();
|
|
3500
|
-
}
|
|
3501
|
-
if (event.kind === EventKinds2.GIFT_WRAP) {
|
|
3502
|
-
this.lastChatEventAt = Date.now();
|
|
3503
|
-
}
|
|
3504
3645
|
try {
|
|
3505
3646
|
if (event.kind === EventKinds2.GIFT_WRAP) {
|
|
3506
3647
|
await this.routeGiftWrap(event);
|
|
@@ -28219,6 +28360,9 @@ var Sphere = class _Sphere {
|
|
|
28219
28360
|
this._transport.setFallbackSince(fallbackTs);
|
|
28220
28361
|
}
|
|
28221
28362
|
await this._transport.setIdentity(this._identity);
|
|
28363
|
+
if (this._transportMux && typeof this._transportMux.rebindToSharedClient === "function") {
|
|
28364
|
+
await this._transportMux.rebindToSharedClient();
|
|
28365
|
+
}
|
|
28222
28366
|
this.emitEvent("identity:changed", {
|
|
28223
28367
|
l1Address: this._identity.l1Address,
|
|
28224
28368
|
directAddress: this._identity.directAddress,
|
|
@@ -28429,7 +28573,12 @@ var Sphere = class _Sphere {
|
|
|
28429
28573
|
this._transportMux = new MultiAddressTransportMux({
|
|
28430
28574
|
relays: nostrTransport.getConfiguredRelays(),
|
|
28431
28575
|
createWebSocket: nostrTransport.getWebSocketFactory(),
|
|
28432
|
-
storage: nostrTransport.getStorageAdapter() ?? void 0
|
|
28576
|
+
storage: nostrTransport.getStorageAdapter() ?? void 0,
|
|
28577
|
+
// #123: share the original transport's NostrClient instead of
|
|
28578
|
+
// opening a second WebSocket per relay. Pass a getter so the
|
|
28579
|
+
// Mux resolves it at connect-time (after the transport finishes
|
|
28580
|
+
// its own connect()).
|
|
28581
|
+
sharedNostrClient: typeof nostrTransport.getNostrClient === "function" ? () => nostrTransport.getNostrClient() : void 0
|
|
28433
28582
|
});
|
|
28434
28583
|
await this._transportMux.connect();
|
|
28435
28584
|
if (typeof nostrTransport.suppressSubscriptions === "function") {
|