@auto-engineer/pipeline 1.82.0 → 1.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +46 -0
- package/dist/src/server/pipeline-server.d.ts +3 -0
- package/dist/src/server/pipeline-server.d.ts.map +1 -1
- package/dist/src/server/pipeline-server.js +38 -0
- package/dist/src/server/pipeline-server.js.map +1 -1
- package/dist/src/server/quiescence-tracker.d.ts +18 -0
- package/dist/src/server/quiescence-tracker.d.ts.map +1 -0
- package/dist/src/server/quiescence-tracker.js +44 -0
- package/dist/src/server/quiescence-tracker.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +9 -0
- package/package.json +3 -3
- package/src/server/pipeline-server.specs.ts +113 -0
- package/src/server/pipeline-server.ts +44 -0
- package/src/server/quiescence-tracker.specs.ts +149 -0
- package/src/server/quiescence-tracker.ts +57 -0
package/ketchup-plan.md
CHANGED
|
@@ -918,6 +918,15 @@ it("should extract graph from emit handler", () => {
|
|
|
918
918
|
|
|
919
919
|
## DONE
|
|
920
920
|
|
|
921
|
+
### Phase 13: PipelineRunCompleted Event (Bursts 107-112) ✅
|
|
922
|
+
|
|
923
|
+
- [x] Burst 107: Create QuiescenceTracker class with increment/decrement/isQuiescent (f4bc85c4)
|
|
924
|
+
- [x] Burst 108: Add debounce logic to QuiescenceTracker with configurable delay (411129cb)
|
|
925
|
+
- [x] Burst 109-111: Wire QuiescenceTracker into PipelineServer, emit PipelineRunCompleted (0011d83b)
|
|
926
|
+
- [x] Burst 112: Verify quiescence tracking handles retries naturally (bb928573)
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
921
930
|
### Command Concurrency Control (Bursts CG-1 to CG-8) ✅
|
|
922
931
|
|
|
923
932
|
- [x] Burst CG-1: Gate registration + passthrough (d0a733c8)
|
package/package.json
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"get-port": "^7.1.0",
|
|
14
14
|
"jose": "^5.9.6",
|
|
15
15
|
"nanoid": "^5.0.0",
|
|
16
|
-
"@auto-engineer/file-store": "1.
|
|
17
|
-
"@auto-engineer/message-bus": "1.
|
|
16
|
+
"@auto-engineer/file-store": "1.83.0",
|
|
17
|
+
"@auto-engineer/message-bus": "1.83.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/cors": "^2.8.17",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
|
-
"version": "1.
|
|
26
|
+
"version": "1.83.0",
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts",
|
|
29
29
|
"test": "vitest run --reporter=dot",
|
|
@@ -2805,4 +2805,117 @@ describe('PipelineServer', () => {
|
|
|
2805
2805
|
await server.stop();
|
|
2806
2806
|
});
|
|
2807
2807
|
});
|
|
2808
|
+
|
|
2809
|
+
describe('PipelineRunCompleted', () => {
|
|
2810
|
+
it('emits PipelineRunCompleted after all commands complete and debounce passes', async () => {
|
|
2811
|
+
const handler = {
|
|
2812
|
+
name: 'DoWork',
|
|
2813
|
+
events: ['WorkDone'],
|
|
2814
|
+
handle: async () => ({ type: 'WorkDone', data: {} }),
|
|
2815
|
+
};
|
|
2816
|
+
|
|
2817
|
+
const server = new PipelineServer({ port: 0 });
|
|
2818
|
+
server.registerCommandHandlers([handler]);
|
|
2819
|
+
await server.start();
|
|
2820
|
+
|
|
2821
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2822
|
+
method: 'POST',
|
|
2823
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2824
|
+
body: JSON.stringify({ type: 'DoWork', data: {} }),
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2828
|
+
|
|
2829
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
2830
|
+
expect(msgs.some((m) => m.message.type === 'PipelineRunCompleted')).toBe(true);
|
|
2831
|
+
|
|
2832
|
+
await server.stop();
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
it('does not emit PipelineRunCompleted while commands are still pending', async () => {
|
|
2836
|
+
let resolveWork: (() => void) | undefined;
|
|
2837
|
+
const handler = {
|
|
2838
|
+
name: 'SlowWork',
|
|
2839
|
+
events: ['SlowWorkDone'],
|
|
2840
|
+
handle: async () => {
|
|
2841
|
+
await new Promise<void>((r) => {
|
|
2842
|
+
resolveWork = r;
|
|
2843
|
+
});
|
|
2844
|
+
return { type: 'SlowWorkDone', data: {} };
|
|
2845
|
+
},
|
|
2846
|
+
};
|
|
2847
|
+
|
|
2848
|
+
const server = new PipelineServer({ port: 0 });
|
|
2849
|
+
server.registerCommandHandlers([handler]);
|
|
2850
|
+
await server.start();
|
|
2851
|
+
|
|
2852
|
+
fetch(`http://localhost:${server.port}/command`, {
|
|
2853
|
+
method: 'POST',
|
|
2854
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2855
|
+
body: JSON.stringify({ type: 'SlowWork', data: {} }),
|
|
2856
|
+
});
|
|
2857
|
+
|
|
2858
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2859
|
+
|
|
2860
|
+
const msgsBeforeComplete = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
2861
|
+
expect(msgsBeforeComplete.some((m) => m.message.type === 'PipelineRunCompleted')).toBe(false);
|
|
2862
|
+
|
|
2863
|
+
if (resolveWork) resolveWork();
|
|
2864
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2865
|
+
|
|
2866
|
+
const msgsAfterComplete = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
2867
|
+
expect(msgsAfterComplete.some((m) => m.message.type === 'PipelineRunCompleted')).toBe(true);
|
|
2868
|
+
|
|
2869
|
+
await server.stop();
|
|
2870
|
+
});
|
|
2871
|
+
|
|
2872
|
+
it('does not emit PipelineRunCompleted while retry commands are dispatched', async () => {
|
|
2873
|
+
let attemptCount = 0;
|
|
2874
|
+
const checkHandler = {
|
|
2875
|
+
name: 'CheckTests',
|
|
2876
|
+
events: ['CheckTestsPassed', 'CheckTestsFailed'],
|
|
2877
|
+
handle: async () => {
|
|
2878
|
+
attemptCount++;
|
|
2879
|
+
if (attemptCount < 3) {
|
|
2880
|
+
return { type: 'CheckTestsFailed', data: { errors: 'fail' } };
|
|
2881
|
+
}
|
|
2882
|
+
return { type: 'CheckTestsPassed', data: {} };
|
|
2883
|
+
},
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
const pipeline = define('retry-test')
|
|
2887
|
+
.on('StartChecks')
|
|
2888
|
+
.emit('CheckTests', () => ({}))
|
|
2889
|
+
.settled(['CheckTests'])
|
|
2890
|
+
.dispatch({ dispatches: ['CheckTests'] }, (events, send) => {
|
|
2891
|
+
const failed = events.CheckTests?.some((e) => e.type === 'CheckTestsFailed');
|
|
2892
|
+
if (failed && attemptCount < 3) {
|
|
2893
|
+
send('CheckTests', {});
|
|
2894
|
+
return { persist: true };
|
|
2895
|
+
}
|
|
2896
|
+
})
|
|
2897
|
+
.build();
|
|
2898
|
+
|
|
2899
|
+
const server = new PipelineServer({ port: 0 });
|
|
2900
|
+
server.registerCommandHandlers([checkHandler]);
|
|
2901
|
+
server.registerPipeline(pipeline);
|
|
2902
|
+
await server.start();
|
|
2903
|
+
|
|
2904
|
+
await fetch(`http://localhost:${server.port}/command`, {
|
|
2905
|
+
method: 'POST',
|
|
2906
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2907
|
+
body: JSON.stringify({ type: 'CheckTests', data: {} }),
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2911
|
+
|
|
2912
|
+
const msgs = await fetchAs<StoredMessage[]>(`http://localhost:${server.port}/messages`);
|
|
2913
|
+
const completedEvents = msgs.filter((m) => m.message.type === 'PipelineRunCompleted');
|
|
2914
|
+
|
|
2915
|
+
expect(completedEvents.length).toBe(1);
|
|
2916
|
+
expect(attemptCount).toBe(3);
|
|
2917
|
+
|
|
2918
|
+
await server.stop();
|
|
2919
|
+
});
|
|
2920
|
+
});
|
|
2808
2921
|
});
|
|
@@ -22,6 +22,7 @@ import { PipelineRuntime } from '../runtime/pipeline-runtime';
|
|
|
22
22
|
import { createPipelineEventStore, type PipelineEventStoreContext } from '../store/pipeline-event-store';
|
|
23
23
|
import { type ConcurrencyConfig, createCommandGate } from './command-gate';
|
|
24
24
|
import { createPhasedBridge } from './phased-bridge';
|
|
25
|
+
import { QuiescenceTracker } from './quiescence-tracker';
|
|
25
26
|
import { SSEManager } from './sse-manager';
|
|
26
27
|
import { createV2RuntimeBridge } from './v2-runtime-bridge';
|
|
27
28
|
|
|
@@ -70,6 +71,7 @@ export class PipelineServer {
|
|
|
70
71
|
private readonly storeFileName?: string;
|
|
71
72
|
private sqliteEventStore?: EventStore;
|
|
72
73
|
private currentSessionId = '';
|
|
74
|
+
private readonly quiescenceTracker: QuiescenceTracker;
|
|
73
75
|
|
|
74
76
|
constructor(config: PipelineServerConfig) {
|
|
75
77
|
this.storeFileName = config.storeFileName;
|
|
@@ -97,6 +99,12 @@ export class PipelineServer {
|
|
|
97
99
|
});
|
|
98
100
|
this.sseManager = new SSEManager();
|
|
99
101
|
this.commandGate = createCommandGate();
|
|
102
|
+
this.quiescenceTracker = new QuiescenceTracker({
|
|
103
|
+
debounceMs: 50,
|
|
104
|
+
onQuiescent: () => {
|
|
105
|
+
void this.emitPipelineRunCompleted();
|
|
106
|
+
},
|
|
107
|
+
});
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
get port(): number {
|
|
@@ -631,6 +639,29 @@ export class PipelineServer {
|
|
|
631
639
|
]);
|
|
632
640
|
}
|
|
633
641
|
|
|
642
|
+
private async emitPipelineRunCompleted(): Promise<void> {
|
|
643
|
+
const correlationId = this.currentSessionId;
|
|
644
|
+
const requestId = `req-${nanoid()}`;
|
|
645
|
+
await this.eventStoreContext.eventStore.appendToStream(`pipeline-${correlationId}`, [
|
|
646
|
+
{
|
|
647
|
+
type: 'DomainEventEmitted',
|
|
648
|
+
data: {
|
|
649
|
+
correlationId,
|
|
650
|
+
requestId,
|
|
651
|
+
eventType: 'PipelineRunCompleted',
|
|
652
|
+
eventData: { correlationId, timestamp: new Date().toISOString() },
|
|
653
|
+
timestamp: new Date(),
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
]);
|
|
657
|
+
const event: Event & { correlationId: string } = {
|
|
658
|
+
type: 'PipelineRunCompleted',
|
|
659
|
+
data: { correlationId, timestamp: new Date().toISOString() },
|
|
660
|
+
correlationId,
|
|
661
|
+
};
|
|
662
|
+
this.sseManager.broadcast(event);
|
|
663
|
+
}
|
|
664
|
+
|
|
634
665
|
private async updateNodeStatus(
|
|
635
666
|
correlationId: string,
|
|
636
667
|
commandName: string,
|
|
@@ -1006,6 +1037,19 @@ export class PipelineServer {
|
|
|
1006
1037
|
command: Command & { correlationId: string; requestId: string },
|
|
1007
1038
|
handler: CommandHandlerWithMetadata,
|
|
1008
1039
|
signal: AbortSignal,
|
|
1040
|
+
): Promise<void> {
|
|
1041
|
+
this.quiescenceTracker.increment();
|
|
1042
|
+
try {
|
|
1043
|
+
await this.executeCommandInner(command, handler, signal);
|
|
1044
|
+
} finally {
|
|
1045
|
+
this.quiescenceTracker.decrement();
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private async executeCommandInner(
|
|
1050
|
+
command: Command & { correlationId: string; requestId: string },
|
|
1051
|
+
handler: CommandHandlerWithMetadata,
|
|
1052
|
+
signal: AbortSignal,
|
|
1009
1053
|
): Promise<void> {
|
|
1010
1054
|
const itemKey = this.extractItemKey(command.type, command.data, command.requestId);
|
|
1011
1055
|
await this.getOrCreateItemStatus(this.currentSessionId, command.type, itemKey, command.requestId);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { QuiescenceTracker } from './quiescence-tracker';
|
|
3
|
+
|
|
4
|
+
describe('QuiescenceTracker', () => {
|
|
5
|
+
describe('isQuiescent', () => {
|
|
6
|
+
it('returns true initially when no commands dispatched', () => {
|
|
7
|
+
const tracker = new QuiescenceTracker();
|
|
8
|
+
|
|
9
|
+
expect(tracker.isQuiescent()).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns false after increment', () => {
|
|
13
|
+
const tracker = new QuiescenceTracker();
|
|
14
|
+
|
|
15
|
+
tracker.increment();
|
|
16
|
+
|
|
17
|
+
expect(tracker.isQuiescent()).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns true after matching increment and decrement', () => {
|
|
21
|
+
const tracker = new QuiescenceTracker();
|
|
22
|
+
tracker.increment();
|
|
23
|
+
|
|
24
|
+
tracker.decrement();
|
|
25
|
+
|
|
26
|
+
expect(tracker.isQuiescent()).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns false when multiple commands pending', () => {
|
|
30
|
+
const tracker = new QuiescenceTracker();
|
|
31
|
+
|
|
32
|
+
tracker.increment();
|
|
33
|
+
tracker.increment();
|
|
34
|
+
tracker.increment();
|
|
35
|
+
tracker.decrement();
|
|
36
|
+
|
|
37
|
+
expect(tracker.isQuiescent()).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns true after all commands complete', () => {
|
|
41
|
+
const tracker = new QuiescenceTracker();
|
|
42
|
+
tracker.increment();
|
|
43
|
+
tracker.increment();
|
|
44
|
+
tracker.increment();
|
|
45
|
+
|
|
46
|
+
tracker.decrement();
|
|
47
|
+
tracker.decrement();
|
|
48
|
+
tracker.decrement();
|
|
49
|
+
|
|
50
|
+
expect(tracker.isQuiescent()).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('remains quiescent after decrement when already at zero', () => {
|
|
54
|
+
const tracker = new QuiescenceTracker();
|
|
55
|
+
|
|
56
|
+
tracker.decrement();
|
|
57
|
+
|
|
58
|
+
expect(tracker.isQuiescent()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('reset', () => {
|
|
63
|
+
it('restores quiescent state when commands were pending', () => {
|
|
64
|
+
const tracker = new QuiescenceTracker();
|
|
65
|
+
tracker.increment();
|
|
66
|
+
tracker.increment();
|
|
67
|
+
|
|
68
|
+
tracker.reset();
|
|
69
|
+
|
|
70
|
+
expect(tracker.isQuiescent()).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('onQuiescent callback with debounce', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
vi.useFakeTimers();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
vi.useRealTimers();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('calls callback after debounce delay when quiescent', () => {
|
|
84
|
+
const callback = vi.fn();
|
|
85
|
+
const tracker = new QuiescenceTracker({ debounceMs: 100, onQuiescent: callback });
|
|
86
|
+
tracker.increment();
|
|
87
|
+
|
|
88
|
+
tracker.decrement();
|
|
89
|
+
|
|
90
|
+
expect(callback).not.toHaveBeenCalled();
|
|
91
|
+
vi.advanceTimersByTime(100);
|
|
92
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not call callback before debounce delay', () => {
|
|
96
|
+
const callback = vi.fn();
|
|
97
|
+
const tracker = new QuiescenceTracker({ debounceMs: 100, onQuiescent: callback });
|
|
98
|
+
tracker.increment();
|
|
99
|
+
|
|
100
|
+
tracker.decrement();
|
|
101
|
+
vi.advanceTimersByTime(50);
|
|
102
|
+
|
|
103
|
+
expect(callback).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('resets debounce timer when new command arrives during debounce', () => {
|
|
107
|
+
const callback = vi.fn();
|
|
108
|
+
const tracker = new QuiescenceTracker({ debounceMs: 100, onQuiescent: callback });
|
|
109
|
+
tracker.increment();
|
|
110
|
+
tracker.decrement();
|
|
111
|
+
vi.advanceTimersByTime(50);
|
|
112
|
+
|
|
113
|
+
tracker.increment();
|
|
114
|
+
|
|
115
|
+
vi.advanceTimersByTime(100);
|
|
116
|
+
expect(callback).not.toHaveBeenCalled();
|
|
117
|
+
|
|
118
|
+
tracker.decrement();
|
|
119
|
+
vi.advanceTimersByTime(100);
|
|
120
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not call callback when reset during debounce', () => {
|
|
124
|
+
const callback = vi.fn();
|
|
125
|
+
const tracker = new QuiescenceTracker({ debounceMs: 100, onQuiescent: callback });
|
|
126
|
+
tracker.increment();
|
|
127
|
+
tracker.decrement();
|
|
128
|
+
vi.advanceTimersByTime(50);
|
|
129
|
+
|
|
130
|
+
tracker.reset();
|
|
131
|
+
|
|
132
|
+
vi.advanceTimersByTime(100);
|
|
133
|
+
expect(callback).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('uses default debounce of 50ms when not specified', () => {
|
|
137
|
+
const callback = vi.fn();
|
|
138
|
+
const tracker = new QuiescenceTracker({ onQuiescent: callback });
|
|
139
|
+
tracker.increment();
|
|
140
|
+
|
|
141
|
+
tracker.decrement();
|
|
142
|
+
|
|
143
|
+
vi.advanceTimersByTime(49);
|
|
144
|
+
expect(callback).not.toHaveBeenCalled();
|
|
145
|
+
vi.advanceTimersByTime(1);
|
|
146
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface QuiescenceTrackerOptions {
|
|
2
|
+
debounceMs?: number;
|
|
3
|
+
onQuiescent?: () => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class QuiescenceTracker {
|
|
7
|
+
private pendingCount = 0;
|
|
8
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
9
|
+
private readonly debounceMs: number;
|
|
10
|
+
private readonly onQuiescent?: () => void;
|
|
11
|
+
|
|
12
|
+
constructor(options: QuiescenceTrackerOptions = {}) {
|
|
13
|
+
this.debounceMs = options.debounceMs ?? 50;
|
|
14
|
+
this.onQuiescent = options.onQuiescent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
increment(): void {
|
|
18
|
+
this.pendingCount++;
|
|
19
|
+
this.cancelDebounce();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
decrement(): void {
|
|
23
|
+
if (this.pendingCount > 0) {
|
|
24
|
+
this.pendingCount--;
|
|
25
|
+
}
|
|
26
|
+
this.scheduleQuiescenceCheck();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isQuiescent(): boolean {
|
|
30
|
+
return this.pendingCount === 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
reset(): void {
|
|
34
|
+
this.pendingCount = 0;
|
|
35
|
+
this.cancelDebounce();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private scheduleQuiescenceCheck(): void {
|
|
39
|
+
const callback = this.onQuiescent;
|
|
40
|
+
if (!this.isQuiescent() || !callback) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.cancelDebounce();
|
|
44
|
+
this.debounceTimer = setTimeout(() => {
|
|
45
|
+
if (this.isQuiescent()) {
|
|
46
|
+
callback();
|
|
47
|
+
}
|
|
48
|
+
}, this.debounceMs);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private cancelDebounce(): void {
|
|
52
|
+
if (this.debounceTimer !== null) {
|
|
53
|
+
clearTimeout(this.debounceTimer);
|
|
54
|
+
this.debounceTimer = null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|