Skip to content

Commit f531e37

Browse files
authored
fix: Resolve circular dependencies in the SDK package (#3361)
## Summary Resolve circular dependencies in the SDK's dependency injection (DI) container by using token-based injection instead of direct class references. This eliminates the need for `delay()` wrappers for `Resends` and `Subscriber` classes. > [!important] > This change is necessary for future ESM compatibility. The current `delay(() => Class)` approach relies on CommonJS module semantics where circular imports can be partially resolved at runtime. In ESM, module bindings are evaluated differently and `delay()` won't be sufficient to break circular dependency cycles. > > Token-based injection provides a clean solution that works in both module systems. ## Changes - Add `tokens.ts` with Symbol-based injection tokens for `Resends` and `Subscriber` - Update `setupTsyringe.ts` to register `Resends` and `Subscriber` classes with the DI container using tokens (with `ContainerScoped` lifecycle) - Replace `@scoped(Lifecycle.ContainerScoped)` decorators with `@injectable()` for `Resends` and `Subscriber` classes (lifecycle is now specified during token registration) - Update `MessagePipelineFactory` to use token injection for `Resends` instead of `delay(() => Resends)` - Update `SubscriberKeyExchange` to use token injection for `Subscriber` (previously injected directly without `delay()`) - Update `StreamrClient` to resolve `Subscriber` and `Resends` using tokens instead of class references ## Limitations and future improvements - Only `Resends` and `Subscriber` are currently registered via tokens; other classes with circular dependency potential could be migrated to this pattern in the future - The `delay()` wrapper is still used for `StreamRegistry` and `GroupKeyManager` in `MessagePipelineFactory`; these could be converted to token injection for consistency
1 parent e2ef301 commit f531e37

File tree

7 files changed

+42
-10
lines changed

7 files changed

+42
-10
lines changed

packages/sdk/src/StreamrClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import { addStreamToStorageNode } from './utils/addStreamToStorageNode'
6565
import { assertCompliantIdentity } from './utils/encryptionCompliance'
6666
import { pOnce } from './utils/promises'
6767
import { convertPeerDescriptorToNetworkPeerDescriptor, createTheGraphClient } from './utils/utils'
68+
import { Tokens } from './tokens'
6869

6970
// TODO: this type only exists to enable tsdoc to generate proper documentation
7071
export type SubscribeOptions = StreamDefinition & ExtraSubscribeOptions
@@ -137,8 +138,8 @@ export class StreamrClient {
137138
this.identity = identity
138139
this.theGraphClient = theGraphClient
139140
this.publisher = container.resolve<Publisher>(Publisher)
140-
this.subscriber = container.resolve<Subscriber>(Subscriber)
141-
this.resends = container.resolve<Resends>(Resends)
141+
this.subscriber = container.resolve<Subscriber>(Tokens.Subscriber)
142+
this.resends = container.resolve<Resends>(Tokens.Resends)
142143
this.node = container.resolve<NetworkNodeFacade>(NetworkNodeFacade)
143144
this.rpcProviderSource = container.resolve(RpcProviderSource)
144145
this.streamRegistry = container.resolve<StreamRegistry>(StreamRegistry)

packages/sdk/src/encryption/SubscriberKeyExchange.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { StreamMessage, StreamMessageType } from '../protocol/StreamMessage'
1010
import { createRandomMsgChainId } from '../publish/messageChain'
1111
import { MessageSigner } from '../signature/MessageSigner'
1212
import { SignatureValidator } from '../signature/SignatureValidator'
13-
import { Subscriber } from '../subscribe/Subscriber'
13+
import type { Subscriber } from '../subscribe/Subscriber'
1414
import { LoggerFactory } from '../utils/LoggerFactory'
1515
import { pOnce, withThrottling } from '../utils/promises'
1616
import { MaxSizedSet } from '../utils/utils'
@@ -21,6 +21,7 @@ import { AsymmetricEncryptionType, ContentType, EncryptionType, GroupKeyRequest,
2121
import { KeyExchangeKeyPair } from './KeyExchangeKeyPair'
2222
import { createCompliantExchangeKeys } from '../utils/encryptionCompliance'
2323
import { StreamrClientError } from '../StreamrClientError'
24+
import { Tokens } from '../tokens'
2425

2526
const MAX_PENDING_REQUEST_COUNT = 50000 // just some limit, we can tweak the number if needed
2627

@@ -51,7 +52,7 @@ export class SubscriberKeyExchange {
5152
signatureValidator: SignatureValidator,
5253
messageSigner: MessageSigner,
5354
store: LocalGroupKeyStore,
54-
subscriber: Subscriber,
55+
@inject(Tokens.Subscriber) subscriber: Subscriber,
5556
@inject(ConfigInjectionToken) config: Pick<StrictStreamrClientConfig, 'encryption' | 'validation'>,
5657
@inject(IdentityInjectionToken) identity: Identity,
5758
loggerFactory: LoggerFactory

packages/sdk/src/setupTsyringe.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,26 @@
11
import 'reflect-metadata'
2+
3+
import { container, Lifecycle } from 'tsyringe'
4+
import { Resends } from './subscribe/Resends'
5+
import { Subscriber } from './subscribe/Subscriber'
6+
import { Tokens } from './tokens'
7+
8+
container.register(
9+
Tokens.Resends,
10+
{
11+
useClass: Resends,
12+
},
13+
{
14+
lifecycle: Lifecycle.ContainerScoped,
15+
}
16+
)
17+
18+
container.register(
19+
Tokens.Subscriber,
20+
{
21+
useClass: Subscriber,
22+
},
23+
{
24+
lifecycle: Lifecycle.ContainerScoped,
25+
}
26+
)

packages/sdk/src/subscribe/MessagePipelineFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import { StreamMessage } from '../protocol/StreamMessage'
1010
import { SignatureValidator } from '../signature/SignatureValidator'
1111
import { LoggerFactory } from '../utils/LoggerFactory'
1212
import { PushPipeline } from '../utils/PushPipeline'
13-
import { Resends } from './Resends'
13+
import type { Resends } from './Resends'
1414
import { MessagePipelineOptions, createMessagePipeline as _createMessagePipeline } from './messagePipeline'
15+
import { Tokens } from '../tokens'
1516

1617
type MessagePipelineFactoryOptions = MarkOptional<Omit<MessagePipelineOptions,
1718
'resends' |
@@ -37,7 +38,7 @@ export class MessagePipelineFactory {
3738

3839
/* eslint-disable indent */
3940
constructor(
40-
@inject(delay(() => Resends)) resends: Resends,
41+
@inject(Tokens.Resends) resends: Resends,
4142
streamStorageRegistry: StreamStorageRegistry,
4243
@inject(delay(() => StreamRegistry)) streamRegistry: StreamRegistry,
4344
signatureValidator: SignatureValidator,

packages/sdk/src/subscribe/Resends.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import random from 'lodash/random'
1414
import sample from 'lodash/sample'
1515
import without from 'lodash/without'
16-
import { Lifecycle, delay, inject, scoped } from 'tsyringe'
16+
import { delay, inject, injectable } from 'tsyringe'
1717
import { ConfigInjectionToken, type StrictStreamrClientConfig } from '../ConfigTypes'
1818
import { StreamrClientError } from '../StreamrClientError'
1919
import { StorageNodeRegistry } from '../contracts/StorageNodeRegistry'
@@ -119,7 +119,7 @@ export const toInternalResendOptions = (options: ResendOptions): InternalResendO
119119
}
120120
}
121121

122-
@scoped(Lifecycle.ContainerScoped)
122+
@injectable()
123123
export class Resends {
124124

125125
private readonly storageNodeRegistry: StorageNodeRegistry

packages/sdk/src/subscribe/Subscriber.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { EthereumAddress, Logger, StreamPartID } from '@streamr/utils'
2-
import { Lifecycle, delay, inject, scoped } from 'tsyringe'
2+
import { delay, inject, injectable } from 'tsyringe'
33
import { NetworkNodeFacade } from '../NetworkNodeFacade'
44
import { LoggerFactory } from '../utils/LoggerFactory'
55
import { MessagePipelineFactory } from './MessagePipelineFactory'
66
import { Subscription } from './Subscription'
77
import { SubscriptionSession } from './SubscriptionSession'
88

9-
@scoped(Lifecycle.ContainerScoped)
9+
@injectable()
1010
export class Subscriber {
1111

1212
private readonly subSessions: Map<StreamPartID, SubscriptionSession> = new Map()

packages/sdk/src/tokens.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const Tokens = {
2+
Resends: Symbol('Resends'),
3+
Subscriber: Symbol('Subscriber')
4+
}

0 commit comments

Comments
 (0)