Skip to content

Conversation

@lukasIO
Copy link
Contributor

@lukasIO lukasIO commented Nov 14, 2025

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Nov 14, 2025

🦋 Changeset detected

Latest commit: 90d9eca

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
livekit-client Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Contributor

github-actions bot commented Nov 14, 2025

size-limit report 📦

Path Size
dist/livekit-client.esm.mjs 86.94 KB (0%)
dist/livekit-client.umd.js 95.48 KB (0%)

Comment on lines 30 to +37
export class WebSocketStream<T extends ArrayBuffer | string = ArrayBuffer | string> {
readonly url: string;

readonly opened: Promise<WebSocketConnection<T>>;
readonly opened: ResultAsync<WebSocketConnection<T>, WebSocketError>;

readonly closed: Promise<WebSocketCloseInfo>;
readonly closed: ResultAsync<WebSocketCloseInfo, WebSocketError>;

readonly close: (closeInfo?: WebSocketCloseInfo) => void;
readonly close!: (closeInfo?: WebSocketCloseInfo) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: if the plan is to eventually migrate to the in-built browser WebsocketStream (and from what I can see, it is on some sort of standards track), it might be worth treating this like vendored code that shouldn't be modified and doing these ResultAsync patches where this is being used in the signal client code.

I can think of pros and cons to both approaches though so I'd be curious to hear your rationale if there is a reason why doing this would be overly burdensome.

Copy link
Contributor Author

@lukasIO lukasIO Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. Was thinking about this too. I have currently less confidence into adapting the standard proposal any time soon. Given that, I think starting with the error propagation at the lowest level sounded like the right choice to me

@lukasIO lukasIO requested a review from 1egoman November 18, 2025 14:00
Copy link
Contributor

@xianshijing-lk xianshijing-lk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some minor comments.

From the general code structure perspective, I think it is the right direction to setup a pattern where the code will throw.
I think moving the throw code to upper layer sounds a good idea, assuming the lower level code stop throwing for expected errors.

Copy link
Contributor

@1egoman 1egoman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Just generally, I think I'm realizing there's some fairly nuanced behavior with neverthrow that doesn't carry over 1:1 with how rust handles errors, so I think I'll need to spend some time digging into some of those nuances.

Comment on lines +274 to +277
const self = this;

const handleSignalConnected = (
connection: WebSocketConnection,
firstMessage?: SignalResponse,
) => {
this.handleSignalConnected(connection, wsTimeout, firstMessage);
};
return withMutex(
safeTry<U, ConnectionError>(async function* () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): When I was first trying to parse through this I saw you swapped all this -> self and it took me a second to figure out exactly where self was being assigned and why you had done this. It looks like (from what I can tell) it's due to the generator function creating a new explicit this scope.

I wanted to propose another option instead - something like async function* () {}.bind(this). There's some discussion here on the pros/cons: supermacro/neverthrow#632. At least as somebody coming into reading this code for the first time, I think it would help but it's minor and something I don't have a strong opinion on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally 50:50 on both, but reading through the issue you linked it mentions two steps being necessary for typescript today to properly bind it, i.e.: return safeTry(function* (this: CurrentClass) {}).bind(this) which would sway me to leave it as is.

reason: unknown,
validateUrl: string,
): Promise<ConnectionError> {
): Promise<Result<never, ConnectionError>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think this should be ResultAsync instead and the returns from this function updated to be errAsync?

UPDATE: reading a little further, this might actually be intended. I'm still leaving this comment though because I think it's relevant to a future comment.

return ResultAsync.fromPromise(Promise.race(settledPromises), (e) => e as E);
}

export type ResultAsyncLike<T, E> = ResultAsync<T, E> | Promise<Result<T, E>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Interesting - in my last comment I had assumed that Promise<Result<T, E>> was a mistake, but this line implies this is very much intentional. Is this a pattern because of async functions always returning promises, ie, the same type of topic as this issue?

Just thinking out loud but I wonder if handling this difference at this level of abstraction is the right place to do it (naively as somebody who hasn't really dug too much into neverthrow in depth yet, this semantic difference is surprising), or if it would be better to write a utility function which can convert any Promise<Result<T, E>> to a ResultAsync<T, E>.

I think I'd need to play around with it a bit on my own to develop a stronger opinion so feel free to keep it how it is for now and we can discuss it in the future once I've been able to dig in further.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it's on purpose but mostly as an intermediate step until we have ported all async internals to the result type.

The helper function is a nice idea, we'd then have to have it include a generic Error though as the Promise could still through and would have to be unwrapped safely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a pattern because of async functions always returning promises, ie, the same type of topic as supermacro/neverthrow#42?

yes, this was the reason and for the helpers I thought having a helper type to accept both would be nice

Comment on lines +1044 to +1045
const self = this;
const restartResultAsync = safeTry(async function* () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(non-blocking): another instance of https://github.com/livekit/client-sdk-js/pull/1747/files#r2546082926 - consider maybe also using .bind(this) here.

@lukasIO lukasIO merged commit 8b8ddbd into main Nov 21, 2025
5 checks passed
@lukasIO lukasIO deleted the lukas/neverthrow-signal branch November 21, 2025 12:07
lukasIO added a commit that referenced this pull request Nov 21, 2025
lukasIO added a commit that referenced this pull request Nov 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants