Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,9 @@

{{#if (eq @autofixRequest.status "in_progress")}}
<div class="bg-gray-800 border border-gray-700 rounded-sm overflow-y-auto relative grow">
<div
class="p-3"
{{did-insert this.handleMarkdownStreamElementInserted}}
{{did-update this.handleDidUpdateAutofixRequestLogstreamId @autofixRequest.logstreamId}}
{{will-destroy this.handleWillDestroyMarkdownStreamElement}}
>
{{#if this.logstream}}
<AnsiStream @content={{this.logstream.content}} class="text-xs whitespace-pre-wrap" />
<div class="p-3" {{logstream-did-update this.handleLogstreamDidUpdate @autofixRequest.logstreamId}}>
{{#if this.logstreamContent}}
<AnsiStream @content={{this.logstreamContent}} class="text-xs whitespace-pre-wrap" />
{{/if}}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import AnalyticsEventTrackerService from 'codecrafters-frontend/services/analytics-event-tracker';
import type Store from '@ember-data/store';
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import Logstream from 'codecrafters-frontend/utils/logstream';
import type AutofixRequestModel from 'codecrafters-frontend/models/autofix-request';
import type ActionCableConsumerService from 'codecrafters-frontend/services/action-cable-consumer';
import type Logstream from 'codecrafters-frontend/utils/logstream';

interface Signature {
Element: HTMLDivElement;
Expand All @@ -17,40 +15,21 @@ interface Signature {
}

export default class AutofixResult extends Component<Signature> {
@service declare store: Store;
@service declare actionCableConsumer: ActionCableConsumerService;
@service declare analyticsEventTracker: AnalyticsEventTrackerService;

@tracked logstream: Logstream | null = null;
@tracked shouldShowFullLog = false;
@tracked logstreamContent: string | null = null;
@tracked diffIsBlurred = true;

@action
handleDidUpdateAutofixRequestLogstreamId() {
if (this.logstream && this.args.autofixRequest.logstreamId !== this.logstream.logstreamId) {
this.logstream.unsubscribe();
this.handleMarkdownStreamElementInserted(); // create a new logstream
}
}
handleLogstreamDidUpdate(logstream: Logstream): void {
this.logstreamContent = logstream.content;

@action
handleLogstreamDidPoll(): void {
// TODO: See if we're doing this too often?
// Ensure we also reload the autofix request to see whether the status has changed
this.args.autofixRequest.reload();
}

@action
handleMarkdownStreamElementInserted() {
this.logstream = new Logstream(this.args.autofixRequest.logstreamId, this.actionCableConsumer, this.store, this.handleLogstreamDidPoll);
this.logstream.subscribe();
}

@action
handleWillDestroyMarkdownStreamElement() {
if (this.logstream) {
this.logstream.unsubscribe();
}
}

@action
toggleBlur() {
if (this.diffIsBlurred) {
Expand Down
50 changes: 50 additions & 0 deletions app/modifiers/logstream-did-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Invokes a callback when a logstream is updated.
// Usage: <div {{logstream-did-update this.handleLogstreamDidUpdate logstreamId}}></div>
import type ActionCableConsumerService from 'codecrafters-frontend/services/action-cable-consumer';
import type Store from '@ember-data/store';
import Logstream from 'codecrafters-frontend/utils/logstream';
import Modifier, { type ArgsFor } from 'ember-modifier';
import { inject as service } from '@ember/service';
import { registerDestructor } from '@ember/destroyable';
import type { Owner } from '@ember/test-helpers/build-owner';

interface Signature {
Args: {
Positional: [(logstream: Logstream) => void, string];
};
}

function cleanup(instance: LogstreamDidUpdateModifier) {
if (instance.logstream) {
instance.logstream.unsubscribe();
instance.logstream = undefined;
}
}

export default class LogstreamDidUpdateModifier extends Modifier<Signature> {
@service declare actionCableConsumer: ActionCableConsumerService;
@service declare store: Store;

logstream?: Logstream;

constructor(owner: unknown, args: ArgsFor<Signature>) {
super(owner as Owner, args);
registerDestructor(this, cleanup);
}

modify(_element: HTMLElement, [callback, logstreamId]: Signature['Args']['Positional']) {
cleanup(this);

this.logstream = new Logstream(logstreamId, this.actionCableConsumer, this.store, () => {
callback(this.logstream!);
});

this.logstream.subscribe();
}
Copy link

Choose a reason for hiding this comment

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

Bug: Logstream Modifier Fails to Unsubscribe Properly

The logstream-did-update modifier creates a new Logstream subscription each time its logstreamId argument changes, but doesn't unsubscribe the previous instance. This causes memory leaks and multiple active subscriptions, as the destructor only cleans up the final Logstream.

Fix in Cursor Fix in Web

}

declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
'logstream-did-update': typeof LogstreamDidUpdateModifier;
}
}
4 changes: 0 additions & 4 deletions app/utils/logstream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ export default class Logstream {
}

subscribeTask = task({ drop: true }, async (): Promise<void> => {
if (this.isSubscribed) {
return;
}

this.isTerminated = false;
this.isSubscribed = false;

Copy link

Choose a reason for hiding this comment

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

Bug: ActionCable Subscription Overwriting Bug

Removing the isSubscribed guard allows multiple ActionCable subscriptions for a single Logstream instance. Repeated calls to subscribe() create new subscriptions, overwriting actionCableSubscription without unsubscribing previous ones. This causes resource leaks, as unsubscribe() only cleans up the latest subscription. The { drop: true } task option prevents concurrent execution, but not sequential duplicate subscriptions.

Fix in Cursor Fix in Web

Expand Down