Skip to content

Commit 2d05094

Browse files
add doc
Signed-off-by: rukmini-basu-da <[email protected]>
1 parent af454b3 commit 2d05094

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import {
5+
JsGetUpdatesResponse,
6+
WebSocketClient,
7+
} from '@canton-network/core-ledger-client'
8+
import { PartyId } from '@canton-network/core-types'
9+
import { Logger } from 'pino'
10+
11+
export type SubscribeOptions = {
12+
beginOffset?: number
13+
verbose?: boolean
14+
partyId: PartyId
15+
} & (
16+
| { interfaceIds: string[]; templateIds?: never }
17+
| { interfaceIds?: never; templateIds: string[] }
18+
)
19+
20+
export class WebSocketSubscriptionError extends Error {
21+
constructor(message: string) {
22+
super(message)
23+
this.name = 'WebSocketSubscriptionError'
24+
}
25+
}
26+
27+
export class InvalidSubscriptionOptionsError extends WebSocketSubscriptionError {
28+
constructor(message: string) {
29+
super(message)
30+
this.name = 'InvalidSubscriptionOptionsError'
31+
}
32+
}
33+
34+
export class WebSocketConnectionError extends WebSocketSubscriptionError {
35+
constructor(message: string) {
36+
super(message)
37+
this.name = 'WebSocketConnectionError'
38+
}
39+
}
40+
41+
export class WebSocketManager {
42+
private wsClient: WebSocketClient
43+
private logger: Logger
44+
45+
constructor({
46+
wsClient,
47+
logger,
48+
}: {
49+
wsClient: WebSocketClient
50+
logger: Logger
51+
}) {
52+
this.wsClient = wsClient
53+
this.logger = logger.child({ component: 'WebSocketManager' })
54+
}
55+
56+
private validateOptions(options: SubscribeOptions): void {
57+
if ('templateIds' in options) {
58+
const templateIds = Array.isArray(options.templateIds)
59+
? options.templateIds
60+
: [options.templateIds]
61+
62+
if (templateIds.length === 0) {
63+
throw new InvalidSubscriptionOptionsError(
64+
'templateIds array cannot be empty.'
65+
)
66+
}
67+
68+
const invalidIds = templateIds.filter(
69+
(id) => typeof id !== 'string'
70+
)
71+
if (invalidIds.length > 0) {
72+
throw new InvalidSubscriptionOptionsError(
73+
`All templateIds must be strings. Invalid ids: ${invalidIds.join(
74+
', '
75+
)}`
76+
)
77+
}
78+
} else if ('interfaceIds' in options) {
79+
const interfaceIds = Array.isArray(options.interfaceIds)
80+
? options.interfaceIds
81+
: [options.interfaceIds]
82+
83+
if (interfaceIds.length === 0) {
84+
throw new InvalidSubscriptionOptionsError(
85+
'interfaceIds array cannot be empty.'
86+
)
87+
}
88+
89+
const invalidIds = interfaceIds.filter(
90+
(id) => typeof id !== 'string'
91+
)
92+
if (invalidIds.length > 0) {
93+
throw new InvalidSubscriptionOptionsError(
94+
`All interfaceIds must be strings. Invalid ids: ${invalidIds.join(
95+
', '
96+
)}`
97+
)
98+
} else {
99+
throw new InvalidSubscriptionOptionsError(
100+
'Subscription options must include either templateIds or interfaceIds.'
101+
)
102+
}
103+
}
104+
105+
if (
106+
options.beginOffset !== undefined &&
107+
typeof options.beginOffset !== 'number'
108+
) {
109+
throw new InvalidSubscriptionOptionsError(
110+
'beginOffset must be a number if provided.'
111+
)
112+
}
113+
}
114+
115+
private normalizeOptions(options: SubscribeOptions) {
116+
{
117+
if ('templateIds' in options && options.templateIds) {
118+
return {
119+
beginExclusive: options.beginOffset ?? 0,
120+
verbose: options.verbose ?? true,
121+
partyId: options.partyId,
122+
templateIds: Array.isArray(options.templateIds)
123+
? options.templateIds
124+
: [options.templateIds],
125+
}
126+
} else {
127+
return {
128+
beginExclusive: options.beginOffset ?? 0,
129+
verbose: options.verbose ?? true,
130+
partyId: options.partyId,
131+
interfaceIds: Array.isArray(options.interfaceIds)
132+
? options.interfaceIds
133+
: [options.interfaceIds],
134+
}
135+
}
136+
}
137+
}
138+
139+
/**
140+
*
141+
* @param options websocket configuration (partyId, templateId/interfaceId, verbose (default = true))
142+
* @returns AsyncIterableIterator of Updates
143+
* @throws InvalidSubscriptionOptionsError if the options is invalid
144+
* @throws WebSocketConnectionError if connection fails
145+
*/
146+
async *subscribe(
147+
options: SubscribeOptions
148+
): AsyncIterableIterator<JsGetUpdatesResponse> {
149+
try {
150+
this.validateOptions(options)
151+
const normalizedOptions = this.normalizeOptions(options)
152+
this.logger.info(
153+
{ options: normalizedOptions },
154+
'Starting WebSocket subscription with options'
155+
)
156+
yield* this.wsClient.subscribeToUpdatesStreaming(normalizedOptions)
157+
} catch (error) {
158+
if (error instanceof InvalidSubscriptionOptionsError) {
159+
this.logger.error(
160+
{ error },
161+
'Failed to subscribe due to invalid options.'
162+
)
163+
throw error
164+
} else {
165+
this.logger.error(
166+
{ error },
167+
'Failed to subscribe due to WebSocket connection error.'
168+
)
169+
throw new WebSocketConnectionError(
170+
'Failed to subscribe due to WebSocket connection error.'
171+
)
172+
}
173+
} finally {
174+
this.logger.info('WebSocket subscription ended.')
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)