Skip to content

Commit 90f46f3

Browse files
committed
Extract SigningService from MessageSigner for clearer separation of concerns
- SigningService: Container-scoped service that owns the signing worker, provides sign() method, and handles worker lifecycle (lazy init, cleanup) - MessageSigner: Now injects SigningService instead of managing worker directly This decouples the worker management from message signing logic. Each StreamrClient gets its own SigningService instance, with the worker destroyed automatically when the client is destroyed. Test utilities updated with createMessageSigner() helper that uses a mock SigningService (no worker) for faster, simpler tests.
1 parent 4145e1e commit 90f46f3

17 files changed

+101
-89
lines changed

packages/sdk/src/signature/MessageSigner.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,19 @@ import { Identity, IdentityInjectionToken } from '../identity/Identity'
44
import { StreamMessage, StreamMessageOptions } from '../protocol/StreamMessage'
55
import { createSignaturePayload } from './createSignaturePayload'
66
import { SignatureType } from '@streamr/trackerless-network'
7-
import { Signing } from './Signing'
8-
import { DestroySignal } from '../DestroySignal'
7+
import { SigningService } from './SigningService'
98

109
@scoped(Lifecycle.ContainerScoped)
1110
export class MessageSigner {
1211
private readonly identity: Identity
13-
private signing: Signing | undefined
12+
private readonly signingService: SigningService
1413

1514
constructor(
1615
@inject(IdentityInjectionToken) identity: Identity,
17-
destroySignal?: DestroySignal
16+
signingService: SigningService
1817
) {
1918
this.identity = identity
20-
destroySignal?.onDestroy.listen(() => this.destroy())
21-
}
22-
23-
private getSigning(): Signing {
24-
return this.signing ??= new Signing()
19+
this.signingService = signingService
2520
}
2621

2722
async createSignedMessage(
@@ -51,7 +46,7 @@ export class MessageSigner {
5146
signatureType: SignatureType,
5247
privateKey: Uint8Array
5348
): Promise<Uint8Array> {
54-
const result = await this.getSigning().createSignature({
49+
const result = await this.signingService.sign({
5550
payloadInput: opts,
5651
privateKey,
5752
signatureType
@@ -63,14 +58,4 @@ export class MessageSigner {
6358

6459
return result.signature
6560
}
66-
67-
/**
68-
* Cleanup worker resources when the signer is no longer needed.
69-
*/
70-
destroy(): void {
71-
if (this.signing) {
72-
this.signing.destroy()
73-
this.signing = undefined
74-
}
75-
}
7661
}

packages/sdk/src/signature/Signing.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Singleton signing service using Web Worker.
3+
* This offloads CPU-intensive cryptographic operations to a separate thread.
4+
* Works in both browser and Node.js environments via platform-specific config.
5+
*
6+
* The worker is lazily initialized on first use and shared across all MessageSigner instances.
7+
*/
8+
import { wrap, releaseProxy, type Remote } from 'comlink'
9+
import { Lifecycle, scoped } from 'tsyringe'
10+
import { createSigningWorker } from '@/createSigningWorker'
11+
import { SigningResult, SigningRequest } from './signingUtils'
12+
import type { SigningWorkerApi } from './SigningWorker'
13+
import { DestroySignal } from '../DestroySignal'
14+
15+
@scoped(Lifecycle.ContainerScoped)
16+
export class SigningService {
17+
private worker: ReturnType<typeof createSigningWorker> | undefined
18+
private workerApi: Remote<SigningWorkerApi> | undefined
19+
20+
constructor(destroySignal: DestroySignal) {
21+
destroySignal.onDestroy.listen(() => this.destroy())
22+
}
23+
24+
private getWorkerApi(): Remote<SigningWorkerApi> {
25+
if (this.workerApi === undefined) {
26+
this.worker = createSigningWorker()
27+
this.workerApi = wrap<SigningWorkerApi>(this.worker)
28+
}
29+
return this.workerApi
30+
}
31+
32+
async sign(request: SigningRequest): Promise<SigningResult> {
33+
return this.getWorkerApi().createSignature(request)
34+
}
35+
36+
destroy(): void {
37+
if (this.workerApi !== undefined) {
38+
this.workerApi[releaseProxy]()
39+
this.workerApi = undefined
40+
}
41+
if (this.worker !== undefined) {
42+
this.worker.terminate()
43+
this.worker = undefined
44+
}
45+
}
46+
}

packages/sdk/test/end-to-end/publish-subscribe-raw.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { StreamID, toUserId } from '@streamr/utils'
2-
import { createTestClient, createTestStream } from '../test-utils/utils'
2+
import { createMessageSigner, createTestClient, createTestStream } from '../test-utils/utils'
33
import { nextValue } from '../../src/utils/iterators'
44
import { EthereumKeyPairIdentity } from '../../src/identity/EthereumKeyPairIdentity'
5-
import { MessageSigner } from '../../src/signature/MessageSigner'
65
import { Wallet } from 'ethers'
76
import { MessageID } from '../../src/protocol/MessageID'
87
import { ContentType, EncryptionType, SignatureType } from '@streamr/trackerless-network'
@@ -29,7 +28,7 @@ describe('publish-subscribe-raw', () => {
2928
})
3029

3130
async function createTestMessage() {
32-
const messageSigner = new MessageSigner(EthereumKeyPairIdentity.fromPrivateKey(publisherWallet.privateKey))
31+
const messageSigner = createMessageSigner(EthereumKeyPairIdentity.fromPrivateKey(publisherWallet.privateKey))
3332
return await messageSigner.createSignedMessage({
3433
messageId: new MessageID(streamId, 0, 123456789, 0, toUserId(publisherWallet.address), 'mock-msgChainId'),
3534
content: new Uint8Array([1, 2, 3]),

packages/sdk/test/integration/Resends.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ import { StreamrClient } from '../../src/StreamrClient'
66
import { GroupKey } from '../../src/encryption/GroupKey'
77
import { StreamPermission } from '../../src/permission'
88
import { MessageFactory } from '../../src/publish/MessageFactory'
9-
import { MessageSigner } from '../../src/signature/MessageSigner'
109
import { SignatureValidator } from '../../src/signature/SignatureValidator'
1110
import { FakeEnvironment } from '../test-utils/fake/FakeEnvironment'
12-
import { createGroupKeyQueue, createStreamRegistry } from '../test-utils/utils'
11+
import { createGroupKeyQueue, createMessageSigner, createStreamRegistry } from '../test-utils/utils'
1312
import { EthereumKeyPairIdentity } from '../../src/identity/EthereumKeyPairIdentity'
1413
import { createStrictConfig } from '../../src/Config'
1514

@@ -45,7 +44,7 @@ describe('Resends', () => {
4544
streamRegistry: createStreamRegistry(),
4645
groupKeyQueue: await createGroupKeyQueue(identity, groupKey),
4746
signatureValidator: mock<SignatureValidator>(),
48-
messageSigner: new MessageSigner(identity),
47+
messageSigner: createMessageSigner(identity),
4948
config: createStrictConfig()
5049
})
5150
// store the encryption key publisher's local group key store

packages/sdk/test/integration/Subscriber2.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { MessageSigner } from '../../src/signature/MessageSigner'
1010
import { Subscription } from '../../src/subscribe/Subscription'
1111
import { FakeEnvironment } from '../test-utils/fake/FakeEnvironment'
1212
import { getPublishTestStreamMessages } from '../test-utils/publish'
13-
import { createTestStream } from '../test-utils/utils'
13+
import { createMessageSigner, createTestStream } from '../test-utils/utils'
1414
import { MessageID } from './../../src/protocol/MessageID'
1515
import { StreamMessage, StreamMessageType } from './../../src/protocol/StreamMessage'
1616
import { ContentType, EncryptionType, SignatureType } from '@streamr/trackerless-network'
@@ -69,7 +69,7 @@ describe('Subscriber', () => {
6969
}
7070
})
7171
const publisherIdentity = EthereumKeyPairIdentity.fromPrivateKey(publisherWallet.privateKey)
72-
messageSigner = new MessageSigner(publisherIdentity)
72+
messageSigner = createMessageSigner(publisherIdentity)
7373
})
7474

7575
afterAll(async () => {

packages/sdk/test/integration/gap-fill.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import { Wallet } from 'ethers'
44
import { mock } from 'jest-mock-extended'
55
import { GroupKey } from '../../src/encryption/GroupKey'
66
import { StreamMessage } from '../../src/protocol/StreamMessage'
7-
import { MessageSigner } from '../../src/signature/MessageSigner'
87
import { SignatureValidator } from '../../src/signature/SignatureValidator'
98
import { FakeEnvironment } from '../test-utils/fake/FakeEnvironment'
10-
import { createGroupKeyQueue, createStreamRegistry, createTestStream, startFailingStorageNode } from '../test-utils/utils'
9+
import { createGroupKeyQueue, createMessageSigner, createStreamRegistry, createTestStream, startFailingStorageNode } from '../test-utils/utils'
1110
import { Stream } from './../../src/Stream'
1211
import { MessageFactory } from './../../src/publish/MessageFactory'
1312
import { EthereumKeyPairIdentity } from '../../src/identity/EthereumKeyPairIdentity'
@@ -45,7 +44,7 @@ describe('gap fill', () => {
4544
streamRegistry: createStreamRegistry(),
4645
groupKeyQueue: await createGroupKeyQueue(identity, GROUP_KEY),
4746
signatureValidator: mock<SignatureValidator>(),
48-
messageSigner: new MessageSigner(identity),
47+
messageSigner: createMessageSigner(identity),
4948
config: createStrictConfig()
5049
})
5150
})

packages/sdk/test/integration/parallel-key-exchange.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import { GroupKey } from '../../src/encryption/GroupKey'
99
import { StreamPermission } from '../../src/permission'
1010
import { StreamMessageType } from '../../src/protocol/StreamMessage'
1111
import { MessageFactory } from '../../src/publish/MessageFactory'
12-
import { MessageSigner } from '../../src/signature/MessageSigner'
1312
import { SignatureValidator } from '../../src/signature/SignatureValidator'
14-
import { createGroupKeyQueue, createStreamRegistry } from '../test-utils/utils'
13+
import { createGroupKeyQueue, createMessageSigner, createStreamRegistry } from '../test-utils/utils'
1514
import { FakeEnvironment } from './../test-utils/fake/FakeEnvironment'
1615
import { EthereumKeyPairIdentity } from '../../src/identity/EthereumKeyPairIdentity'
1716
import { createStrictConfig } from '../../src/Config'
@@ -73,7 +72,7 @@ describe('parallel key exchange', () => {
7372
}),
7473
groupKeyQueue: await createGroupKeyQueue(identity, publisher.groupKey),
7574
signatureValidator: mock<SignatureValidator>(),
76-
messageSigner: new MessageSigner(identity),
75+
messageSigner: createMessageSigner(identity),
7776
config: createStrictConfig()
7877
})
7978
for (let i = 0; i < MESSAGE_COUNT_PER_PUBLISHER; i++) {

packages/sdk/test/integration/waitForStorage.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { StreamPermission } from '../../src/permission'
66
import { MessageSigner } from '../../src/signature/MessageSigner'
77
import { FakeEnvironment } from '../test-utils/fake/FakeEnvironment'
88
import { FakeStorageNode } from '../test-utils/fake/FakeStorageNode'
9-
import { MOCK_CONTENT, createRandomIdentity, createRelativeTestStreamId } from '../test-utils/utils'
9+
import { MOCK_CONTENT, createMessageSigner, createRandomIdentity, createRelativeTestStreamId } from '../test-utils/utils'
1010
import { MessageID } from './../../src/protocol/MessageID'
1111
import { StreamMessageType } from './../../src/protocol/StreamMessage'
1212
import { randomUserId } from '@streamr/test-utils'
@@ -23,7 +23,7 @@ describe('waitForStorage', () => {
2323
let environment: FakeEnvironment
2424

2525
beforeEach(async () => {
26-
messageSigner = new MessageSigner(await createRandomIdentity())
26+
messageSigner = createMessageSigner(await createRandomIdentity())
2727
environment = new FakeEnvironment()
2828
client = environment.createClient()
2929
stream = await client.createStream({

packages/sdk/test/test-utils/utils.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ import { StreamMessage } from '../../src/protocol/StreamMessage'
4949
import { GroupKeyQueue } from '../../src/publish/GroupKeyQueue'
5050
import { MessageFactory } from '../../src/publish/MessageFactory'
5151
import { MessageSigner } from '../../src/signature/MessageSigner'
52+
import { SigningService } from '../../src/signature/SigningService'
53+
import { createSignatureFromData } from '../../src/signature/signingUtils'
5254
import { SignatureValidator } from '../../src/signature/SignatureValidator'
5355
import { LoggerFactory } from '../../src/utils/LoggerFactory'
5456
import { counterId } from '../../src/utils/utils'
@@ -59,6 +61,25 @@ import { StreamIDBuilder } from '../../src/StreamIDBuilder'
5961

6062
const logger = new Logger('sdk-test-utils')
6163

64+
/**
65+
* Creates a mock SigningService that performs signing synchronously on the main thread.
66+
* Use this in tests instead of the real SigningService which spawns a worker.
67+
*/
68+
export function createMockSigningService(): SigningService {
69+
return {
70+
sign: createSignatureFromData,
71+
destroy: () => {}
72+
} as unknown as SigningService
73+
}
74+
75+
/**
76+
* Creates a MessageSigner for testing purposes.
77+
* Uses a mock SigningService that doesn't spawn a worker.
78+
*/
79+
export function createMessageSigner(identity: Identity): MessageSigner {
80+
return new MessageSigner(identity, createMockSigningService())
81+
}
82+
6283
export function mockLoggerFactory(clientId?: string): LoggerFactory {
6384
return new LoggerFactory({
6485
id: clientId ?? counterId('TestCtx'),
@@ -152,7 +173,7 @@ export const createMockMessage = async (
152173
}),
153174
groupKeyQueue: await createGroupKeyQueue(identity, opts.encryptionKey, opts.nextEncryptionKey),
154175
signatureValidator: mock<SignatureValidator>(),
155-
messageSigner: new MessageSigner(identity)
176+
messageSigner: createMessageSigner(identity)
156177
})
157178
const DEFAULT_CONTENT = {}
158179
const plainContent = opts.content ?? DEFAULT_CONTENT

0 commit comments

Comments
 (0)